@glittering0717/ai-browser 2.1.3 → 2.1.4

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.3",
3
+ "version": "2.1.4",
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": {
@@ -18,9 +18,24 @@
18
18
  }
19
19
  /* ---- 第一行:tab 条 ---- */
20
20
  #tabs-row { display: flex; align-items: stretch; height: 36px; flex: none; }
21
- #tabs { display: flex; flex: 1; overflow: hidden; }
21
+ /* 标签超过可视宽度时必须能横向滚动,否则后面的标签点不到、也关不掉。
22
+ 原来是 overflow:hidden —— 超出部分被裁掉且没有任何滚动途径,标签一多
23
+ 就等于"后半截标签永久不可用"。 */
24
+ #tabs {
25
+ display: flex; flex: 1; min-width: 0;
26
+ overflow-x: auto; overflow-y: hidden;
27
+ -webkit-app-region: no-drag;
28
+ }
29
+ /* 细滚动条:既提示"还有更多标签",又不占视觉高度 */
30
+ #tabs::-webkit-scrollbar { height: 3px; }
31
+ #tabs::-webkit-scrollbar-track { background: transparent; }
32
+ #tabs::-webkit-scrollbar-thumb { background: #3a3a3a; border-radius: 2px; }
33
+ #tabs::-webkit-scrollbar-thumb:hover { background: #4d4d4d; }
22
34
  .tab {
23
35
  display: flex; align-items: center;
36
+ /* flex:none:不参与收缩。否则标签会被一路压到 min-width 再溢出,
37
+ 配合滚动后应该保持各自宽度、让容器去滚。 */
38
+ flex: none;
24
39
  max-width: 200px; min-width: 80px; padding: 0 12px; height: 36px;
25
40
  cursor: pointer; border-right: 1px solid #2a2a2a;
26
41
  transition: background 0.15s;
@@ -91,7 +106,15 @@
91
106
  </div>
92
107
 
93
108
  <script>
94
- const { ipcRenderer } = require('electron');
109
+ // ipcRenderer 在 Electron 主窗口里才有。直接用浏览器/测试页打开本文件时
110
+ // require 会抛错——以前这会让整个 <script> 中断,连布局和滚动监听都不生效
111
+ // (UI 完全死掉)。现在降级成空操作:交互没了,但标签栏仍然看得见、滚得动。
112
+ let ipcRenderer = null;
113
+ try { ({ ipcRenderer } = require('electron')); } catch (e) { /* 非 Electron 环境 */ }
114
+ function send(ch, payload) { try { ipcRenderer.send(ch, payload); } catch (e) {} }
115
+ function invoke(ch, payload) {
116
+ return ipcRenderer ? ipcRenderer.invoke(ch, payload) : Promise.reject(new Error('no ipc'));
117
+ }
95
118
 
96
119
  const $ = (id) => document.getElementById(id);
97
120
  const tabsEl = $('tabs'), addrEl = $('addr'), statusEl = $('status');
@@ -115,28 +138,78 @@ function applyNavState(s) {
115
138
  addrEl.style.borderColor = s.loading ? '#e0a33e' : '';
116
139
  }
117
140
 
141
+ let lastIds = ''; // 上次渲染的 tab id 序列 —— 只有它变了才重建 DOM
142
+ let lastActiveId = null;
143
+
144
+ // 把指定 tab 滚进可视区(只在它确实在视区外时才动,避免无谓跳动)。
145
+ // 用 getBoundingClientRect 而不是 offsetLeft:后者依赖 offsetParent,
146
+ // 在 flex + 滚动容器里算出来的是错的值。
147
+ function scrollTabIntoView(id) {
148
+ const el = tabsEl.querySelector('.tab[data-id="' + id + '"]');
149
+ if (!el) return;
150
+ const cr = tabsEl.getBoundingClientRect(), er = el.getBoundingClientRect();
151
+ if (er.left < cr.left) tabsEl.scrollLeft -= (cr.left - er.left) + 8;
152
+ else if (er.right > cr.right) tabsEl.scrollLeft += (er.right - cr.right) + 8;
153
+ }
154
+
118
155
  async function renderTabs() {
119
156
  try {
120
- const tabs = await ipcRenderer.invoke('tab:list');
121
- tabsEl.innerHTML = '';
122
- for (const tab of tabs) {
123
- const el = document.createElement('div');
124
- el.className = 'tab' + (tab.active ? ' active' : '');
125
- const title = (tab.title || tab.url || 'New Tab').slice(0, 30);
126
- el.innerHTML = '<span class="tab-id">[' + tab.id + ']</span>' +
127
- '<span class="tab-title">' + escapeHtml(title) + '</span>' +
128
- '<span class="tab-close" data-id="' + tab.id + '">&times;</span>';
129
- el.addEventListener('click', function (e) {
130
- if (e.target.classList.contains('tab-close')) {
131
- ipcRenderer.send('tab:close', parseInt(e.target.dataset.id));
132
- } else {
133
- ipcRenderer.send('tab:activate', tab.id);
134
- }
135
- });
136
- tabsEl.appendChild(el);
157
+ const tabs = await invoke('tab:list');
158
+ const ids = tabs.map(function (t) { return t.id; }).join(',');
159
+
160
+ if (ids !== lastIds) {
161
+ // 结构变了(增/删 tab):重建 DOM,但必须保住横向滚动位置 ——
162
+ // 否则在右端关掉一个标签后视图会跳回最左,后面的标签又摸不着了。
163
+ const prevScroll = tabsEl.scrollLeft;
164
+ tabsEl.innerHTML = '';
165
+ for (const tab of tabs) {
166
+ const el = document.createElement('div');
167
+ el.className = 'tab' + (tab.active ? ' active' : '');
168
+ el.dataset.id = tab.id;
169
+ el.innerHTML = '<span class="tab-id">[' + tab.id + ']</span>' +
170
+ '<span class="tab-title">' + escapeHtml((tab.title || tab.url || 'New Tab').slice(0, 30)) + '</span>' +
171
+ '<span class="tab-close" data-id="' + tab.id + '">&times;</span>';
172
+ el.addEventListener('click', function (e) {
173
+ // 从 this.dataset 取 id,不用闭包里的 tab —— 增量更新时闭包会过期。
174
+ const id = parseInt(this.dataset.id);
175
+ if (e.target.classList.contains('tab-close')) send('tab:close', id);
176
+ else send('tab:activate', id);
177
+ });
178
+ tabsEl.appendChild(el);
179
+ }
180
+ lastIds = ids;
181
+ tabsEl.scrollLeft = prevScroll;
182
+ } else {
183
+ // 结构没变:只改标题和激活态。绝不重建 —— 这里每 1.5s 就跑一次,
184
+ // 一旦重建,用户刚滚到的位置就被重置,标签一多就永远滚不回去。
185
+ for (let i = 0; i < tabs.length && i < tabsEl.children.length; i++) {
186
+ const t = tabs[i], el = tabsEl.children[i];
187
+ el.classList.toggle('active', !!t.active);
188
+ const titleEl = el.querySelector('.tab-title');
189
+ const title = (t.title || t.url || 'New Tab').slice(0, 30);
190
+ if (titleEl && titleEl.textContent !== title) titleEl.textContent = title;
191
+ }
137
192
  }
193
+
194
+ // 激活的 tab 换人了(含 agent 用 browse_tabs 切的情况)就把它滚进来;
195
+ // 已经可见时上面的计算不会产生位移。
196
+ const active = tabs.find(function (t) { return t.active; });
197
+ const activeId = active ? active.id : null;
198
+ if (activeId !== null && activeId !== lastActiveId) scrollTabIntoView(activeId);
199
+ if (activeId !== null) lastActiveId = activeId;
138
200
  } catch (e) { /* ipcRenderer 尚未就绪 */ }
139
201
  }
202
+
203
+ // 横向滚动:鼠标滚轮是垂直的,标签栏要的是左右滚(浏览器惯例)。
204
+ // overflow 改成 auto 只是"能滚",没有这一条仍然只能拖滚动条/用触控板横扫。
205
+ tabsEl.addEventListener('wheel', function (e) {
206
+ if (tabsEl.scrollWidth <= tabsEl.clientWidth) return;
207
+ const d = Math.abs(e.deltaY) >= Math.abs(e.deltaX) ? e.deltaY : e.deltaX;
208
+ if (!d) return;
209
+ tabsEl.scrollLeft += d;
210
+ e.preventDefault();
211
+ }, { passive: false });
212
+
140
213
  function escapeHtml(s) {
141
214
  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
142
215
  }
@@ -147,27 +220,27 @@ function escapeHtml(s) {
147
220
  // 所以显示数据全部由主进程推送,本地不缓存。
148
221
  let activeTab = null;
149
222
 
150
- ipcRenderer.on('nav:state', (_e, s) => applyNavState(s));
223
+ if (ipcRenderer) ipcRenderer.on('nav:state', (_e, s) => applyNavState(s));
151
224
 
152
- backBtn.addEventListener('click', () => ipcRenderer.send('nav:back'));
153
- fwdBtn.addEventListener('click', () => ipcRenderer.send('nav:forward'));
154
- reloadBtn.addEventListener('click', () => ipcRenderer.send('nav:reload'));
155
- stopBtn.addEventListener('click', () => ipcRenderer.send('nav:stop'));
225
+ backBtn.addEventListener('click', () => send('nav:back'));
226
+ fwdBtn.addEventListener('click', () => send('nav:forward'));
227
+ reloadBtn.addEventListener('click', () => send('nav:reload'));
228
+ stopBtn.addEventListener('click', () => send('nav:stop'));
156
229
 
157
230
  addrEl.addEventListener('keydown', (e) => {
158
231
  if (e.key !== 'Enter') return;
159
232
  const v = addrEl.value.trim();
160
233
  if (!v) return;
161
234
  addrEl.blur();
162
- ipcRenderer.send('nav:go', v);
235
+ send('nav:go', v);
163
236
  });
164
237
  // 聚焦时全选,方便直接输入覆盖(浏览器惯例)
165
238
  addrEl.addEventListener('focus', () => addrEl.select());
166
239
 
167
- $('new-tab').addEventListener('click', () => ipcRenderer.send('tab:new'));
240
+ $('new-tab').addEventListener('click', () => send('tab:new'));
168
241
 
169
242
  // 初始拉一次状态,然后 tab 条照旧定时刷新
170
- ipcRenderer.invoke('nav:state').then((s) => { if (s) applyNavState(s); });
243
+ invoke('nav:state').then((s) => { if (s) applyNavState(s); }).catch(() => {});
171
244
  renderTabs();
172
245
  setInterval(renderTabs, 1500);
173
246
  </script>
@@ -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.3';
5
+ export const VERSION = '2.1.4';