@glittering0717/ai-browser 2.1.5 → 2.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glittering0717/ai-browser",
3
- "version": "2.1.5",
3
+ "version": "2.1.6",
4
4
  "description": "AI-native browser — renders pixels for humans, structured semantic-tree API for agents. No screenshots, no OCR, no coordinate guessing.",
5
5
  "main": "src/main/index.js",
6
6
  "bin": {
package/src/main/index.js CHANGED
@@ -15,6 +15,13 @@ import errorBanner from '../shared/error_banner.cjs';
15
15
  const __filename = fileURLToPath(import.meta.url);
16
16
  const __dirname = path.dirname(__filename);
17
17
 
18
+ // 窗口标题用的产品名。
19
+ // 不能靠 Electron 默认值:主窗口自己没有加载任何 document,Electron 会退回
20
+ // `app.getName()`,而它读的是 package.json 的 name —— 用户在标题栏、Mission
21
+ // Control、窗口切换器里看到的是 `@glittering0717/ai-browser`(npm 包名)。
22
+ // 产品名必须是我们自己声明的常量。
23
+ const APP_DISPLAY_NAME = 'AI Browser';
24
+
18
25
  // CRITICAL: set userData BEFORE app.whenReady — session/cookie storage
19
26
  // is initialized during ready, so setting it after loses persistence.
20
27
  app.setPath('userData', config.userDataDir);
@@ -358,6 +365,9 @@ function createWindow() {
358
365
  mainWindow = new BrowserWindow({
359
366
  width: 1280,
360
367
  height: 800,
368
+ // 见文件头 APP_DISPLAY_NAME:不显式给 title 会显示成 npm 包名。
369
+ // 真实显示值由 updateWindowTitle() 按当前标签页覆盖。
370
+ title: APP_DISPLAY_NAME,
361
371
  // Windows/Linux 的任务栏图标来自这里(macOS 走 app.dock.setIcon,
362
372
  // 见 applyAppIcon:Dock 跟的是进程可执行文件,不是窗口选项)。
363
373
  icon: appIconPath() || undefined,
@@ -370,6 +380,11 @@ function createWindow() {
370
380
  },
371
381
  });
372
382
 
383
+ // 窗口标题统一由 updateWindowTitle() 控制(当前标签页标题 + 产品名)。
384
+ // 主窗口自己的 webContents 不加载页面,但 renderer 重启的瞬间可能出现空
385
+ // document.title,Electron 会拿它把标题清成空字符串 —— 这里直接掐断。
386
+ mainWindow.webContents.on('page-title-updated', (e) => { e.preventDefault(); });
387
+
373
388
  // Tab bar — needs nodeIntegration for ipcRenderer
374
389
  tabBarView = new BrowserView({
375
390
  webPreferences: {
@@ -763,8 +778,33 @@ function activeWebContents() {
763
778
  return view.webContents;
764
779
  }
765
780
 
781
+ /**
782
+ * 窗口标题 = "当前页标题 — AI Browser",没有页标题时只显示产品名。
783
+ *
784
+ * 为什么要有:多窗口/多桌面环境下,标题栏是用户唯一能识别"这个窗口在哪个页面"
785
+ * 的地方;原先它是 npm 包名,既不是产品名也不带任何页面信息。
786
+ * 只做「变了才设」——pushNavState 由事件 + 1.5s 轮询双重驱动,无条件 setTitle
787
+ * 会让系统在每次轮询时都收到一次标题变更通知。
788
+ */
789
+ let lastWindowTitle = '';
790
+ function updateWindowTitle() {
791
+ if (!mainWindow || mainWindow.isDestroyed()) return;
792
+ const wc = activeWebContents();
793
+ let pageTitle = '';
794
+ try { pageTitle = wc ? String(wc.getTitle() || '').trim() : ''; } catch (_) { pageTitle = ''; }
795
+ const next = pageTitle && pageTitle !== APP_DISPLAY_NAME
796
+ ? `${pageTitle} — ${APP_DISPLAY_NAME}`
797
+ : APP_DISPLAY_NAME;
798
+ if (next === lastWindowTitle) return;
799
+ lastWindowTitle = next;
800
+ try { mainWindow.setTitle(next); } catch (_) { /* 窗口在关闭竞态里 */ }
801
+ }
802
+
766
803
  /** 把地址栏需要的一切状态推给 tab bar renderer。 */
767
804
  function pushNavState() {
805
+ // 标题先更新:它不依赖 tabBarView 是否存活(横幅/崩溃场景下 tabBarView
806
+ // 可能已被销毁,但窗口标题仍应反映真实页面)。
807
+ updateWindowTitle();
768
808
  if (!tabBarView || tabBarView.webContents.isDestroyed()) return;
769
809
  const wc = activeWebContents();
770
810
  const state = wc ? {
@@ -166,6 +166,9 @@ async function renderTabs() {
166
166
  const el = document.createElement('div');
167
167
  el.className = 'tab' + (tab.active ? ' active' : '');
168
168
  el.dataset.id = tab.id;
169
+ // 标题在 UI 上被截断到 30 字(标签宽度有限),完整标题 + 地址放 tooltip:
170
+ // 否则长标题站点的标签永远看不出是哪一篇("【第 12 章】…"全一样)。
171
+ el.title = tabTooltip(tab);
169
172
  el.innerHTML = '<span class="tab-id">[' + tab.id + ']</span>' +
170
173
  '<span class="tab-title">' + escapeHtml((tab.title || tab.url || 'New Tab').slice(0, 30)) + '</span>' +
171
174
  '<span class="tab-close" data-id="' + tab.id + '">&times;</span>';
@@ -188,6 +191,8 @@ async function renderTabs() {
188
191
  const titleEl = el.querySelector('.tab-title');
189
192
  const title = (t.title || t.url || 'New Tab').slice(0, 30);
190
193
  if (titleEl && titleEl.textContent !== title) titleEl.textContent = title;
194
+ const tip = tabTooltip(t);
195
+ if (el.title !== tip) el.title = tip;
191
196
  }
192
197
  }
193
198
 
@@ -210,6 +215,25 @@ tabsEl.addEventListener('wheel', function (e) {
210
215
  e.preventDefault();
211
216
  }, { passive: false });
212
217
 
218
+ // tab 的完整标题 + 地址,用作 tooltip。标签上只显示前 30 字,
219
+ // 长标题站点(论文/连载/同名页面)不靠 tooltip 根本分辨不出。
220
+ // 中键点击关闭标签(浏览器惯例)。用事件委托而不是给每个标签挂:
221
+ // 标签是增量更新的,逐个挂监听会在重建后丢一半。
222
+ tabsEl.addEventListener('auxclick', function (e) {
223
+ if (e.button !== 1) return; // 1 = 中键
224
+ const el = e.target && e.target.closest ? e.target.closest('.tab') : null;
225
+ if (!el || !el.dataset.id) return;
226
+ e.preventDefault();
227
+ send('tab:close', parseInt(el.dataset.id));
228
+ });
229
+
230
+ function tabTooltip(t) {
231
+ const title = String(t.title || '').trim();
232
+ const url = String(t.url || '').trim();
233
+ if (title && url && title !== url) return title + '\n' + url;
234
+ return title || url || 'New Tab';
235
+ }
236
+
213
237
  function escapeHtml(s) {
214
238
  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
215
239
  }
@@ -2,4 +2,4 @@
2
2
  // Kept in JS (not imported from package.json) to avoid Node import-attribute
3
3
  // (JSON module) compatibility issues inside Electron. package.json "version"
4
4
  // is manually synced to this value; tests assert they match.
5
- export const VERSION = '2.1.5';
5
+ export const VERSION = '2.1.6';