coding-tool-x 3.5.4 → 3.5.6

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +8 -4
  3. package/dist/web/assets/{Analytics-CmN09J9U.js → Analytics-CRNCHeui.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-CeTAPmep.js → ConfigTemplates-C0erJdo2.js} +1 -1
  5. package/dist/web/assets/{Home-BYtCM3rK.js → Home-CL5z6Q4d.js} +1 -1
  6. package/dist/web/assets/{PluginManager-OAH1eMO0.js → PluginManager-hDx0XMO_.js} +1 -1
  7. package/dist/web/assets/{ProjectList-B0pIy1cv.js → ProjectList-BNsz96av.js} +1 -1
  8. package/dist/web/assets/{SessionList-DbB6ASiA.js → SessionList-CG1UhFo3.js} +1 -1
  9. package/dist/web/assets/{SkillManager-wp1dhL1z.js → SkillManager-D6Vwpajh.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-Ce6wQoKb.js → WorkspaceManager-C3TjeOPy.js} +1 -1
  11. package/dist/web/assets/{icons-DlxD2wZJ.js → icons-CQuif85v.js} +1 -1
  12. package/dist/web/assets/index-GuER-BmS.js +2 -0
  13. package/dist/web/assets/{index-B02wDWNC.css → index-VGAxnLqi.css} +1 -1
  14. package/dist/web/index.html +3 -3
  15. package/package.json +1 -1
  16. package/src/commands/stats.js +41 -4
  17. package/src/index.js +1 -0
  18. package/src/server/api/codex-sessions.js +6 -3
  19. package/src/server/api/dashboard.js +25 -1
  20. package/src/server/api/gemini-sessions.js +6 -3
  21. package/src/server/api/hooks.js +17 -1
  22. package/src/server/api/opencode-sessions.js +6 -3
  23. package/src/server/api/plugins.js +24 -33
  24. package/src/server/api/sessions.js +6 -3
  25. package/src/server/codex-proxy-server.js +24 -59
  26. package/src/server/gemini-proxy-server.js +25 -66
  27. package/src/server/index.js +6 -4
  28. package/src/server/opencode-proxy-server.js +24 -59
  29. package/src/server/proxy-server.js +18 -30
  30. package/src/server/services/base/response-usage-parser.js +187 -0
  31. package/src/server/services/codex-sessions.js +107 -9
  32. package/src/server/services/network-access.js +14 -0
  33. package/src/server/services/notification-hooks.js +175 -16
  34. package/src/server/services/plugins-service.js +502 -44
  35. package/src/server/services/proxy-log-helper.js +21 -3
  36. package/src/server/services/session-launch-command.js +81 -0
  37. package/src/server/services/sessions.js +103 -33
  38. package/src/server/services/statistics-service.js +7 -0
  39. package/src/server/websocket-server.js +25 -1
  40. package/dist/web/assets/index-CHwVofQH.js +0 -2
@@ -10,6 +10,7 @@ const ALL_SESSIONS_CACHE_TTL_MS = 20 * 1000;
10
10
  const PROJECTS_CACHE_TTL_MS = 300 * 1000;
11
11
  const PROJECT_SESSIONS_CACHE_TTL_MS = 120 * 1000;
12
12
  const FAST_META_READ_BYTES = 64 * 1024;
13
+ const MAX_SESSION_META_SUMMARY_CACHE_ENTRIES = 5000;
13
14
  const EMPTY_COUNTS = Object.freeze({ projectCount: 0, sessionCount: 0 });
14
15
 
15
16
  let countsCache = {
@@ -32,6 +33,8 @@ let allSessionsCache = {
32
33
  value: []
33
34
  };
34
35
 
36
+ let sessionMetaSummaryCache = new Map();
37
+
35
38
  const CODEX_PROJECTS_CACHE_KEY = `${CacheKeys.PROJECTS}codex`;
36
39
  const codexSessionCacheKeys = new Set();
37
40
 
@@ -39,6 +42,47 @@ function getCodexSessionsCacheKey(projectName) {
39
42
  return `${CacheKeys.SESSIONS}codex:${projectName}`;
40
43
  }
41
44
 
45
+ function getSessionMetaSummaryCacheKey(filePath, fileMeta = {}) {
46
+ return `${filePath}:${fileMeta.mtimeMs || 0}:${fileMeta.size || 0}`;
47
+ }
48
+
49
+ function getCachedSessionMetaSummary(filePath, fileMeta) {
50
+ if (!fileMeta) {
51
+ return null;
52
+ }
53
+ return sessionMetaSummaryCache.get(getSessionMetaSummaryCacheKey(filePath, fileMeta)) || null;
54
+ }
55
+
56
+ function setCachedSessionMetaSummary(filePath, fileMeta, summary) {
57
+ if (!fileMeta || !summary?.payload) {
58
+ return;
59
+ }
60
+
61
+ const cacheKey = getSessionMetaSummaryCacheKey(filePath, fileMeta);
62
+ sessionMetaSummaryCache.delete(cacheKey);
63
+ sessionMetaSummaryCache.set(cacheKey, summary);
64
+
65
+ while (sessionMetaSummaryCache.size > MAX_SESSION_META_SUMMARY_CACHE_ENTRIES) {
66
+ const oldestKey = sessionMetaSummaryCache.keys().next().value;
67
+ if (!oldestKey) {
68
+ break;
69
+ }
70
+ sessionMetaSummaryCache.delete(oldestKey);
71
+ }
72
+ }
73
+
74
+ function pruneSessionMetaSummaryCache(files = []) {
75
+ const activeKeys = new Set(
76
+ files.map(file => getSessionMetaSummaryCacheKey(file.filePath, file))
77
+ );
78
+
79
+ for (const cacheKey of sessionMetaSummaryCache.keys()) {
80
+ if (!activeKeys.has(cacheKey)) {
81
+ sessionMetaSummaryCache.delete(cacheKey);
82
+ }
83
+ }
84
+ }
85
+
42
86
  /**
43
87
  * 获取会话目录
44
88
  */
@@ -128,6 +172,7 @@ function scanSessionFiles() {
128
172
  expiresAt,
129
173
  value: new Map(parsed.map(file => [file.sessionId, file]))
130
174
  };
175
+ pruneSessionMetaSummaryCache(parsed);
131
176
 
132
177
  return parsed;
133
178
  }
@@ -145,7 +190,7 @@ function getAllSessions() {
145
190
  const files = scanSessionFiles();
146
191
 
147
192
  const parsed = files.map(file => {
148
- const fastSummary = readSessionMetaSummaryFast(file.filePath);
193
+ const fastSummary = readSessionMetaSummaryFast(file.filePath, file);
149
194
  let session = null;
150
195
 
151
196
  if (fastSummary && fastSummary.payload) {
@@ -216,7 +261,7 @@ function normalizeSession(codexSession) {
216
261
  filePath: filePath || '',
217
262
  gitBranch: meta.git?.branch || null,
218
263
  firstMessage: preview || null,
219
- forkedFrom: null, // Codex 不支持 fork
264
+ forkedFrom: null,
220
265
 
221
266
  // 额外的 Codex 特有字段(前端可能需要)
222
267
  source: 'codex'
@@ -655,9 +700,10 @@ function forkSession(sessionId) {
655
700
 
656
701
  const newFileName = `rollout-${timestamp}-${newSessionId}.jsonl`;
657
702
  const newFilePath = path.join(targetDir, newFileName);
703
+ const rewrittenContent = rewriteForkedCodexSessionContent(content, newSessionId, now.toISOString());
658
704
 
659
705
  // 写入新文件
660
- fs.writeFileSync(newFilePath, content, 'utf8');
706
+ fs.writeFileSync(newFilePath, rewrittenContent, 'utf8');
661
707
 
662
708
  // 保存 fork 关系(复用 Claude Code 的 fork 关系存储)
663
709
  const { getForkRelations, saveForkRelations } = require('./sessions');
@@ -674,6 +720,50 @@ function forkSession(sessionId) {
674
720
  };
675
721
  }
676
722
 
723
+ function rewriteForkedCodexSessionContent(content, newSessionId, nowIsoTimestamp) {
724
+ const lines = String(content || '').split('\n');
725
+
726
+ return lines.map((line) => {
727
+ if (!line.trim()) {
728
+ return line;
729
+ }
730
+
731
+ let parsed;
732
+ try {
733
+ parsed = JSON.parse(line);
734
+ } catch (err) {
735
+ return line;
736
+ }
737
+
738
+ if (parsed.type === 'session_meta' && parsed.payload && typeof parsed.payload === 'object') {
739
+ return JSON.stringify({
740
+ ...parsed,
741
+ timestamp: nowIsoTimestamp,
742
+ payload: {
743
+ ...parsed.payload,
744
+ id: newSessionId,
745
+ timestamp: nowIsoTimestamp
746
+ }
747
+ });
748
+ }
749
+
750
+ if (parsed.type === 'event' && parsed.event && typeof parsed.event === 'object' && parsed.event.type === 'session_start') {
751
+ const nextEvent = { ...parsed.event };
752
+ if (typeof nextEvent.session_id === 'string' && nextEvent.session_id.trim()) {
753
+ nextEvent.session_id = newSessionId;
754
+ }
755
+
756
+ return JSON.stringify({
757
+ ...parsed,
758
+ timestamp: nowIsoTimestamp,
759
+ event: nextEvent
760
+ });
761
+ }
762
+
763
+ return line;
764
+ }).join('\n');
765
+ }
766
+
677
767
  /**
678
768
  * 获取会话排序(按项目)
679
769
  * @param {string} projectName - 项目名称
@@ -803,7 +893,12 @@ function extractCodexPreviewFromResponseItem(payload = {}) {
803
893
  return text.substring(0, 100);
804
894
  }
805
895
 
806
- function readSessionMetaSummaryFast(filePath) {
896
+ function readSessionMetaSummaryFast(filePath, fileMeta = null) {
897
+ const cached = getCachedSessionMetaSummary(filePath, fileMeta);
898
+ if (cached) {
899
+ return cached;
900
+ }
901
+
807
902
  let fd;
808
903
  try {
809
904
  fd = fs.openSync(filePath, 'r');
@@ -841,7 +936,9 @@ function readSessionMetaSummaryFast(filePath) {
841
936
  }
842
937
 
843
938
  if (!payload) return null;
844
- return { payload, preview };
939
+ const summary = { payload, preview };
940
+ setCachedSessionMetaSummary(filePath, fileMeta, summary);
941
+ return summary;
845
942
  } catch (err) {
846
943
  return null;
847
944
  } finally {
@@ -855,8 +952,8 @@ function readSessionMetaSummaryFast(filePath) {
855
952
  }
856
953
  }
857
954
 
858
- function readSessionMetaPayloadFast(filePath) {
859
- const summary = readSessionMetaSummaryFast(filePath);
955
+ function readSessionMetaPayloadFast(filePath, fileMeta = null) {
956
+ const summary = readSessionMetaSummaryFast(filePath, fileMeta);
860
957
  return summary?.payload || null;
861
958
  }
862
959
 
@@ -881,7 +978,7 @@ function calculateProjectAndSessionCounts() {
881
978
 
882
979
  const projectNames = new Set();
883
980
  sessions.forEach((session) => {
884
- const payload = readSessionMetaPayloadFast(session.filePath);
981
+ const payload = readSessionMetaPayloadFast(session.filePath, session);
885
982
  const projectName = extractCodexProjectNameFromMeta(payload || {});
886
983
  if (projectName) {
887
984
  projectNames.add(projectName);
@@ -932,5 +1029,6 @@ module.exports = {
932
1029
  saveSessionOrder,
933
1030
  getProjectOrder,
934
1031
  saveProjectOrder,
935
- getProjectAndSessionCounts
1032
+ getProjectAndSessionCounts,
1033
+ rewriteForkedCodexSessionContent
936
1034
  };
@@ -50,6 +50,19 @@ function isSameOriginRequest(req) {
50
50
  }
51
51
  }
52
52
 
53
+ function isRemoteMutationAllowed(envValue) {
54
+ if (envValue === undefined || envValue === null) {
55
+ return true;
56
+ }
57
+
58
+ const normalized = String(envValue).trim().toLowerCase();
59
+ if (!normalized) {
60
+ return true;
61
+ }
62
+
63
+ return !['0', 'false', 'no', 'off'].includes(normalized);
64
+ }
65
+
53
66
  function createRemoteMutationGuard(options = {}) {
54
67
  const enabled = options.enabled === true;
55
68
  const allowRemoteMutation = options.allowRemoteMutation === true;
@@ -112,6 +125,7 @@ module.exports = {
112
125
  isLoopbackAddress,
113
126
  isLoopbackRequest,
114
127
  isSameOriginRequest,
128
+ isRemoteMutationAllowed,
115
129
  createRemoteMutationGuard,
116
130
  createRemoteRouteGuard,
117
131
  createSameOriginGuard
@@ -5,6 +5,7 @@ const http = require('http');
5
5
  const https = require('https');
6
6
  const { execSync, execFileSync } = require('child_process');
7
7
  const { PATHS, NATIVE_PATHS } = require('../../config/paths');
8
+ const { loadConfig } = require('../../config/loader');
8
9
  const { loadUIConfig, saveUIConfig } = require('./ui-config');
9
10
  const codexSettingsManager = require('./codex-settings-manager');
10
11
  const geminiSettingsManager = require('./gemini-settings-manager');
@@ -57,6 +58,13 @@ function getFeishuConfig() {
57
58
  };
58
59
  }
59
60
 
61
+ function getBrowserNotificationConfig() {
62
+ const uiConfig = loadUIConfig();
63
+ return {
64
+ enabled: uiConfig.browserNotification?.enabled === true
65
+ };
66
+ }
67
+
60
68
  function applyClaudeDisablePreference(uiConfig = {}, claudeEnabled) {
61
69
  const nextConfig = (uiConfig && typeof uiConfig === 'object') ? { ...uiConfig } : {};
62
70
  if (claudeEnabled) {
@@ -67,7 +75,7 @@ function applyClaudeDisablePreference(uiConfig = {}, claudeEnabled) {
67
75
  return nextConfig;
68
76
  }
69
77
 
70
- function saveNotificationUiConfig(feishu = {}, claudeEnabled) {
78
+ function saveNotificationUiConfig(feishu = {}, browser = {}, claudeEnabled) {
71
79
  let uiConfig = loadUIConfig();
72
80
  if (typeof claudeEnabled === 'boolean') {
73
81
  uiConfig = applyClaudeDisablePreference(uiConfig, claudeEnabled);
@@ -76,6 +84,9 @@ function saveNotificationUiConfig(feishu = {}, claudeEnabled) {
76
84
  enabled: feishu.enabled === true,
77
85
  webhookUrl: feishu.webhookUrl || ''
78
86
  };
87
+ uiConfig.browserNotification = {
88
+ enabled: browser.enabled === true
89
+ };
79
90
  saveUIConfig(uiConfig);
80
91
  }
81
92
 
@@ -115,6 +126,84 @@ function removeNotifyScript() {
115
126
  }
116
127
  }
117
128
 
129
+ function getNotificationSourceLabel(source = 'claude') {
130
+ switch (String(source || '').trim().toLowerCase()) {
131
+ case 'codex':
132
+ return 'Codex CLI';
133
+ case 'gemini':
134
+ return 'Gemini CLI';
135
+ case 'opencode':
136
+ return 'OpenCode';
137
+ default:
138
+ return 'Claude Code';
139
+ }
140
+ }
141
+
142
+ function resolveFallbackNotificationMessage(source = 'claude', eventType = '') {
143
+ const normalizedSource = String(source || '').trim().toLowerCase();
144
+ const normalizedEventType = String(eventType || '').trim();
145
+
146
+ if (normalizedSource === 'codex') {
147
+ if (normalizedEventType === 'agent-turn-complete') {
148
+ return 'Codex CLI 回合已完成 | 等待交互';
149
+ }
150
+ return 'Codex CLI 已返回结果 | 等待交互';
151
+ }
152
+
153
+ if (normalizedSource === 'gemini') {
154
+ return 'Gemini CLI 回合已完成 | 等待交互';
155
+ }
156
+
157
+ if (normalizedSource === 'opencode') {
158
+ if (normalizedEventType === 'session.error') {
159
+ return 'OpenCode 会话异常,请检查日志';
160
+ }
161
+ return 'OpenCode 响应已完成 | 等待交互';
162
+ }
163
+
164
+ return 'Claude Code 任务已完成 | 等待交互';
165
+ }
166
+
167
+ function buildBrowserNotificationEndpoint() {
168
+ try {
169
+ const config = loadConfig();
170
+ const port = Number(config?.ports?.webUI);
171
+ const resolvedPort = Number.isFinite(port) && port > 0 ? port : 19999;
172
+ return `http://127.0.0.1:${resolvedPort}/api/hooks/browser-event`;
173
+ } catch (error) {
174
+ return 'http://127.0.0.1:19999/api/hooks/browser-event';
175
+ }
176
+ }
177
+
178
+ function buildBrowserNotificationPayload(input = {}) {
179
+ const rawSource = String(input?.source || '').trim().toLowerCase();
180
+ const source = ['claude', 'codex', 'gemini', 'opencode'].includes(rawSource) ? rawSource : 'claude';
181
+ const eventType = String(input?.eventType || input?.event_type || '').trim();
182
+ const rawMessage = String(input?.message || '').trim();
183
+ const timestamp = Number(input?.timestamp);
184
+
185
+ return {
186
+ title: String(input?.title || '').trim() || 'Coding Tool',
187
+ source,
188
+ sourceLabel: getNotificationSourceLabel(source),
189
+ mode: normalizeType(input?.mode),
190
+ eventType,
191
+ message: rawMessage || resolveFallbackNotificationMessage(source, eventType),
192
+ timestamp: Number.isFinite(timestamp) ? timestamp : Date.now()
193
+ };
194
+ }
195
+
196
+ function emitBrowserNotificationEvent(input = {}) {
197
+ const browser = getBrowserNotificationConfig();
198
+ if (!browser.enabled) {
199
+ return false;
200
+ }
201
+
202
+ const { broadcastBrowserNotification } = require('../websocket-server');
203
+ broadcastBrowserNotification(buildBrowserNotificationPayload(input));
204
+ return true;
205
+ }
206
+
118
207
  function parseManagedType(input) {
119
208
  const value = String(input || '');
120
209
  const matches = [
@@ -433,8 +522,10 @@ function runWindowsPowerShellCommand(command) {
433
522
  });
434
523
  }
435
524
 
436
- function generateNotifyScript(feishu = {}) {
525
+ function generateNotifyScript(feishu = {}, browser = {}) {
437
526
  const feishuEnabled = feishu.enabled === true && !!feishu.webhookUrl;
527
+ const browserEnabled = browser.enabled === true;
528
+ const browserEndpoint = browserEnabled ? buildBrowserNotificationEndpoint() : '';
438
529
 
439
530
  return `#!/usr/bin/env node
440
531
  // Coding Tool 通知脚本 - 自动生成,请勿手动修改
@@ -446,6 +537,8 @@ const { execSync, execFileSync } = require('child_process')
446
537
 
447
538
  const FEISHU_ENABLED = ${feishuEnabled ? 'true' : 'false'}
448
539
  const FEISHU_WEBHOOK_URL = ${JSON.stringify(feishuEnabled ? feishu.webhookUrl : '')}
540
+ const BROWSER_ENABLED = ${browserEnabled ? 'true' : 'false'}
541
+ const BROWSER_ENDPOINT = ${JSON.stringify(browserEndpoint)}
449
542
 
450
543
  function readArg(name) {
451
544
  const prefix = \`\${name}=\`
@@ -483,21 +576,25 @@ function readOptionalPayload() {
483
576
  return null
484
577
  }
485
578
 
486
- function resolveMessage(source, eventType, payload) {
487
- const effectiveEventType = eventType || payload?.type || payload?.hook_event?.event_type || ''
579
+ function resolveEventType(eventType, payload) {
580
+ return eventType || payload?.type || payload?.hook_event?.event_type || ''
581
+ }
582
+
583
+ function resolveMessage(source, effectiveEventType) {
584
+ const normalizedSource = String(source || '').trim().toLowerCase()
488
585
 
489
- if (source === 'codex') {
586
+ if (normalizedSource === 'codex') {
490
587
  if (effectiveEventType === 'agent-turn-complete') {
491
588
  return 'Codex CLI 回合已完成 | 等待交互'
492
589
  }
493
590
  return 'Codex CLI 已返回结果 | 等待交互'
494
591
  }
495
592
 
496
- if (source === 'gemini') {
593
+ if (normalizedSource === 'gemini') {
497
594
  return 'Gemini CLI 回合已完成 | 等待交互'
498
595
  }
499
596
 
500
- if (source === 'opencode') {
597
+ if (normalizedSource === 'opencode') {
501
598
  if (effectiveEventType === 'session.error') {
502
599
  return 'OpenCode 会话异常,请检查日志'
503
600
  }
@@ -628,15 +725,63 @@ function sendFeishu(message, source) {
628
725
  })
629
726
  }
630
727
 
728
+ function sendBrowserNotification(payload) {
729
+ if (!BROWSER_ENABLED || !BROWSER_ENDPOINT) {
730
+ return Promise.resolve()
731
+ }
732
+
733
+ return new Promise((resolve) => {
734
+ try {
735
+ const urlObj = new URL(BROWSER_ENDPOINT)
736
+ const data = JSON.stringify(payload)
737
+ const options = {
738
+ hostname: urlObj.hostname,
739
+ port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
740
+ path: urlObj.pathname + urlObj.search,
741
+ method: 'POST',
742
+ headers: {
743
+ 'Content-Type': 'application/json',
744
+ 'Content-Length': Buffer.byteLength(data)
745
+ },
746
+ timeout: 5000
747
+ }
748
+
749
+ const requestModule = urlObj.protocol === 'https:' ? https : http
750
+ const request = requestModule.request(options, () => resolve())
751
+ request.on('error', () => resolve())
752
+ request.on('timeout', () => {
753
+ request.destroy()
754
+ resolve()
755
+ })
756
+ request.write(data)
757
+ request.end()
758
+ } catch (error) {
759
+ resolve()
760
+ }
761
+ })
762
+ }
763
+
631
764
  (async () => {
632
765
  const source = readArg('--source') || 'claude'
633
766
  const mode = readArg('--mode') || readArg('--cc-notify-type') || 'notification'
634
767
  const eventType = readArg('--event-type') || ''
635
768
  const payload = readOptionalPayload()
636
- const message = resolveMessage(source, eventType, payload)
637
-
638
- notify(mode === 'dialog' ? 'dialog' : 'notification', message)
639
- await sendFeishu(message, source)
769
+ const effectiveEventType = resolveEventType(eventType, payload)
770
+ const normalizedMode = mode === 'dialog' ? 'dialog' : 'notification'
771
+ const message = resolveMessage(source, effectiveEventType)
772
+ const timestamp = Date.now()
773
+
774
+ notify(normalizedMode, message)
775
+ await Promise.all([
776
+ sendFeishu(message, source),
777
+ sendBrowserNotification({
778
+ source,
779
+ mode: normalizedMode,
780
+ eventType: effectiveEventType,
781
+ message,
782
+ timestamp
783
+ })
784
+ ])
640
785
  })().catch(() => {
641
786
  process.exit(0)
642
787
  })
@@ -856,9 +1001,9 @@ function runWindowsPowerShellCommand(command) {
856
1001
  `;
857
1002
  }
858
1003
 
859
- function writeNotifyScript(feishu = {}) {
1004
+ function writeNotifyScript(feishu = {}, browser = getBrowserNotificationConfig()) {
860
1005
  ensureParentDir(PATHS.notifyHook);
861
- fs.writeFileSync(PATHS.notifyHook, generateNotifyScript(feishu), { mode: 0o755 });
1006
+ fs.writeFileSync(PATHS.notifyHook, generateNotifyScript(feishu, browser), { mode: 0o755 });
862
1007
  }
863
1008
 
864
1009
  function getClaudeHookStatus() {
@@ -1167,6 +1312,7 @@ function getNotificationSettings() {
1167
1312
  success: true,
1168
1313
  platform: os.platform(),
1169
1314
  feishu: getFeishuConfig(),
1315
+ browser: getBrowserNotificationConfig(),
1170
1316
  platforms: {
1171
1317
  claude: getClaudeHookStatus(),
1172
1318
  codex: getCodexHookStatus(),
@@ -1185,6 +1331,7 @@ function normalizePlatformInput(platform = {}) {
1185
1331
 
1186
1332
  function saveNotificationSettings(input = {}) {
1187
1333
  const existingFeishu = getFeishuConfig();
1334
+ const existingBrowser = getBrowserNotificationConfig();
1188
1335
  const requestedWebhookUrl = String(input?.feishu?.webhookUrl || '').trim();
1189
1336
  const feishu = {
1190
1337
  enabled: input?.feishu?.enabled === true,
@@ -1193,6 +1340,11 @@ function saveNotificationSettings(input = {}) {
1193
1340
  if (feishu.enabled && feishu.webhookUrl) {
1194
1341
  validateFeishuWebhookUrl(feishu.webhookUrl);
1195
1342
  }
1343
+ const browser = {
1344
+ enabled: input?.browser !== undefined
1345
+ ? input?.browser?.enabled === true
1346
+ : existingBrowser.enabled === true
1347
+ };
1196
1348
  const platforms = {
1197
1349
  claude: normalizePlatformInput(input?.platforms?.claude),
1198
1350
  codex: normalizePlatformInput(input?.platforms?.codex),
@@ -1200,11 +1352,11 @@ function saveNotificationSettings(input = {}) {
1200
1352
  opencode: normalizePlatformInput(input?.platforms?.opencode)
1201
1353
  };
1202
1354
 
1203
- saveNotificationUiConfig(feishu, platforms.claude.enabled);
1355
+ saveNotificationUiConfig(feishu, browser, platforms.claude.enabled);
1204
1356
 
1205
1357
  const hasManagedPlatform = Object.values(platforms).some((platform) => platform.enabled);
1206
1358
  if (hasManagedPlatform) {
1207
- writeNotifyScript(feishu);
1359
+ writeNotifyScript(feishu, browser);
1208
1360
  }
1209
1361
 
1210
1362
  saveClaudeHook(platforms.claude.enabled, platforms.claude.type);
@@ -1347,6 +1499,9 @@ function buildLegacyClaudeSaveInput(input = {}, currentSettings = getNotificatio
1347
1499
  : {
1348
1500
  enabled: currentSettings?.feishu?.enabled === true,
1349
1501
  webhookUrl: currentSettings?.feishu?.webhookUrl || ''
1502
+ },
1503
+ browser: {
1504
+ enabled: currentSettings?.browser?.enabled === true
1350
1505
  }
1351
1506
  };
1352
1507
  }
@@ -1356,6 +1511,7 @@ function getLegacyClaudeHookSettings() {
1356
1511
  success: true,
1357
1512
  stopHook: parseStopHookStatus(readClaudeSettings()),
1358
1513
  feishu: getFeishuConfig(),
1514
+ browser: getBrowserNotificationConfig(),
1359
1515
  platform: os.platform()
1360
1516
  };
1361
1517
  }
@@ -1553,7 +1709,7 @@ function syncManagedNotificationAssets() {
1553
1709
  const hasManagedPlatform = Object.values(settings?.platforms || {}).some((platform) => platform?.enabled === true);
1554
1710
 
1555
1711
  if (hasManagedPlatform) {
1556
- writeNotifyScript(settings.feishu || {});
1712
+ writeNotifyScript(settings.feishu || {}, settings.browser || {});
1557
1713
  } else {
1558
1714
  removeNotifyScript();
1559
1715
  }
@@ -1575,6 +1731,7 @@ module.exports = {
1575
1731
  getLegacyClaudeHookSettings,
1576
1732
  saveNotificationSettings,
1577
1733
  saveLegacyClaudeHookSettings,
1734
+ emitBrowserNotificationEvent,
1578
1735
  testNotification,
1579
1736
  initDefaultHooks,
1580
1737
  syncManagedNotificationAssets,
@@ -1600,6 +1757,8 @@ module.exports = {
1600
1757
  buildGeminiCommand,
1601
1758
  buildStopHookCommand,
1602
1759
  buildClaudeCommand,
1760
+ buildBrowserNotificationEndpoint,
1761
+ buildBrowserNotificationPayload,
1603
1762
  buildOpenCodePluginContent,
1604
1763
  getOpenCodeManagedPluginPath,
1605
1764
  generateNotifyScript,