dsh-browser-plus 0.5.0 → 0.5.1
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/CHANGELOG.md +131 -144
- package/README.en.md +27 -8
- package/README.md +27 -8
- package/client/index.js +185 -0
- package/cordis.patch.yml +24 -0
- package/docs/README.md +1 -1
- package/docs/SOAK-CHECKLIST.md +1 -1
- package/docs/tool-reference.md +5 -3
- package/docs/user-guide.md +20 -4
- package/lib/browser/runtime.d.ts +14 -1
- package/lib/browser/runtime.js +28 -0
- package/lib/browser/types.d.ts +96 -6
- package/lib/browser-electron/chrome-state.d.ts +24 -0
- package/lib/browser-electron/entry.d.ts +12 -5
- package/lib/browser-electron/entry.js +5 -2
- package/lib/browser-electron/host-main.d.ts +2 -1
- package/lib/browser-electron/host-main.js +205 -8
- package/lib/browser-electron/page-chrome.js +249 -14
- package/lib/browser-electron/provider.d.ts +60 -2
- package/lib/browser-electron/provider.js +300 -50
- package/lib/browser-electron/remote-host.d.ts +3 -1
- package/lib/browser-electron/remote-host.js +42 -1
- package/lib/client.js +185 -0
- package/lib/command-browser/index.d.ts +20 -0
- package/lib/command-browser/index.js +35 -0
- package/lib/http-browser/index.d.ts +28 -0
- package/lib/http-browser/index.js +110 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +11 -0
- package/lib/task-todos/index.d.ts +25 -0
- package/lib/task-todos/index.js +100 -0
- package/lib/tool-browser/index.js +190 -95
- package/package.json +27 -2
- package/scripts/build-client.mjs +20 -0
- package/scripts/smoke-browser-tools.mjs +84 -1
- package/scripts/smoke-chrome-world.mjs +6 -4
- package/scripts/test-orb-drag.mjs +83 -0
- package/src/browser/runtime.ts +36 -0
- package/src/browser/types.ts +98 -6
- package/src/browser-electron/chrome-state.ts +18 -0
- package/src/browser-electron/entry.ts +17 -7
- package/src/browser-electron/host-main.ts +207 -11
- package/src/browser-electron/page-chrome.ts +249 -14
- package/src/browser-electron/provider.ts +328 -50
- package/src/browser-electron/remote-host.ts +49 -2
- package/src/command-browser/index.ts +61 -0
- package/src/http-browser/index.ts +139 -0
- package/src/index.ts +13 -0
- package/src/task-todos/index.ts +114 -0
- package/src/tool-browser/index.ts +193 -96
|
@@ -99,6 +99,34 @@ export function buildPageChromeScript(bindingToken = '', surface: ChromeSurface
|
|
|
99
99
|
'#taskPanel .task-meta { color:#b8c5d6; }',
|
|
100
100
|
'#taskPanel .task-latest { color:#acd0ff; }',
|
|
101
101
|
'#taskPanel .task-error { color:#ffb1b1; }',
|
|
102
|
+
// ── 悬浮球:Agent 现在在做什么 ─────────────────────────────────────────────
|
|
103
|
+
// 宿主整层是 pointer-events:none,交互元素各自开回来 —— 漏了这一句球就只是画在那里:
|
|
104
|
+
// 拖不动、悬停也不出卡片(用户实测报「拖不动」就是它)。
|
|
105
|
+
'#orb { pointer-events:auto; position:fixed; right:18px; bottom:18px; width:44px; height:44px; z-index:2147483000; display:grid; place-items:center; box-sizing:border-box; border:1px solid rgba(255,255,255,.14); border-radius:50%; background:#292a2d; color:#e8eaed; cursor:pointer; box-shadow:0 1px 2px rgba(0,0,0,.5),0 8px 20px rgba(0,0,0,.34); opacity:.62; transition:opacity .18s ease,background .18s ease,border-color .18s ease; }',
|
|
106
|
+
'#orb:hover, #orb:focus-visible, #orb.dragging { opacity:1; background:#35363a; border-color:rgba(255,255,255,.24); }',
|
|
107
|
+
'#orb .orb-mark { width:21px; height:21px; opacity:.88; }',
|
|
108
|
+
'#orb .orb-ring { position:absolute; inset:-3px; border-radius:50%; border:2px solid transparent; border-top-color:#8ab4f8; border-right-color:#8ab4f8; opacity:0; transition:opacity .18s ease; animation:dshOrbSpin .85s linear infinite; }',
|
|
109
|
+
'#orb.working .orb-ring { opacity:1; }',
|
|
110
|
+
'@keyframes dshOrbSpin { to { transform:rotate(360deg); } }',
|
|
111
|
+
'#orb .orb-badge { position:absolute; right:-3px; bottom:-3px; box-sizing:border-box; min-width:17px; height:17px; padding:0 4px; border-radius:9px; background:#5f6368; color:#fff; font:600 10px/17px -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif; text-align:center; box-shadow:0 0 0 2px #202124; }',
|
|
112
|
+
'#orb .orb-badge:empty { display:none; }',
|
|
113
|
+
'#orb.done .orb-badge { background:#34a853; }',
|
|
114
|
+
'#orb.failed .orb-badge { background:#d93025; }',
|
|
115
|
+
'#orb.waiting .orb-badge { background:#f9ab00; color:#202124; }',
|
|
116
|
+
'#orbCard { pointer-events:auto; position:fixed; right:18px; bottom:74px; width:308px; max-height:342px; z-index:2147483000; display:none; flex-direction:column; overflow:hidden; border:1px solid #3c4043; border-radius:10px; background:#292a2d; color:#e8eaed; box-shadow:0 8px 24px rgba(0,0,0,.5); font:12px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif; }',
|
|
117
|
+
'#orbCard.open { display:flex; }',
|
|
118
|
+
'#orbCard .orb-head { flex:none; display:flex; align-items:baseline; justify-content:space-between; gap:8px; padding:9px 12px; border-bottom:1px solid #3c4043; font-weight:500; }',
|
|
119
|
+
'#orbCard .orb-task { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:#9aa0a6; font-weight:400; font-size:11px; }',
|
|
120
|
+
'#orbCard .orb-list { overflow:auto; padding:6px 0; }',
|
|
121
|
+
'#orbCard .orb-item { display:grid; grid-template-columns:16px minmax(0,1fr); column-gap:8px; align-items:start; padding:5px 12px; }',
|
|
122
|
+
'#orbCard .orb-mark { display:grid; place-items:center; width:16px; height:16px; color:#9aa0a6; font-size:11px; }',
|
|
123
|
+
'#orbCard .orb-item.in_progress .orb-mark { color:#8ab4f8; }',
|
|
124
|
+
'#orbCard .orb-item.completed .orb-mark { color:#81c995; }',
|
|
125
|
+
'#orbCard .orb-item.completed .orb-text { color:#9aa0a6; }',
|
|
126
|
+
'#orbCard .orb-text { min-width:0; overflow-wrap:anywhere; }',
|
|
127
|
+
'#orbCard .orb-spin { width:10px; height:10px; border:1.5px solid transparent; border-top-color:#8ab4f8; border-right-color:#8ab4f8; border-radius:50%; animation:dshOrbSpin .9s linear infinite; }',
|
|
128
|
+
'#orbCard .orb-foot { flex:none; padding:8px 12px; border-top:1px solid #3c4043; color:#8ab4f8; font-size:11px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }',
|
|
129
|
+
'#orbCard .orb-empty { padding:10px 12px; color:#9aa0a6; }',
|
|
102
130
|
'#trail #trailList { flex:1; overflow-y:auto; padding:8px 12px; }',
|
|
103
131
|
'#trail .activity-day { color:#accbff; font:11px/1.4 -apple-system,BlinkMacSystemFont,"SF Pro Display","Segoe UI",system-ui,sans-serif; text-align:center; margin:6px 0 5px; }',
|
|
104
132
|
'#trail .activity-item { transition:background .12s ease; position:relative; display:grid; grid-template-columns:54px 16px minmax(0,1fr); column-gap:7px; align-items:start; padding:8px 0; }',
|
|
@@ -284,14 +312,13 @@ export function buildPageChromeScript(bindingToken = '', surface: ChromeSurface
|
|
|
284
312
|
// and 'page' is the copy that stays in the page (the popups). Popups cannot live
|
|
285
313
|
// in the frame: it is 84px tall and a menu would be clipped.
|
|
286
314
|
const chromeCssText = toolbarCss + '\n#toolbarRevealZone,#toolbarHide{display:none!important}'
|
|
287
|
-
// #bookmarkBar
|
|
288
|
-
//
|
|
289
|
-
|
|
315
|
+
// #bookmarkBar 现在由 frame 那份画:帧视图会跟着开关长到 118px(84 + 34),
|
|
316
|
+
// 书签栏正好落在 84..118。页面视图从 118 开始 —— 所以页面**一点都不用让位**,
|
|
317
|
+
// 站点的 position:fixed 头部也就永远不会被压住(这正是这一改动要修的)。
|
|
318
|
+
+ (surface === 'frame' ? '\n#panel, .glass-panel, #mainMenu, #findBar, #toast, #orb, #orbCard { display:none !important }' : '')
|
|
290
319
|
+ (surface === 'page'
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
// very top of it and the popups hang just below the frame.
|
|
294
|
-
+ '\n#bookmarkBar { top:0 !important }'
|
|
320
|
+
// 书签栏由 frame 那份画,页面这份连标签栏一起藏掉,弹层挂在帧下方。
|
|
321
|
+
? '\n#tabstrip, #bar, #bookmarkBar, #toolbarRevealZone, #toolbarHide { display:none !important }'
|
|
295
322
|
+ '\n#panel, .glass-panel, #mainMenu, #findBar { top:8px !important }'
|
|
296
323
|
: '')
|
|
297
324
|
|
|
@@ -364,6 +391,11 @@ export function buildPageChromeScript(bindingToken = '', surface: ChromeSurface
|
|
|
364
391
|
'<div class="task-head"><span>浏览器任务</span><button id="taskClose" class="task-close" type="button" aria-label="关闭任务">×</button></div>',
|
|
365
392
|
'<div id="taskList"></div>',
|
|
366
393
|
'</div>',
|
|
394
|
+
// 悬浮球(右下角):Agent 正在做什么。它不属于 menus —— 页面上的点击不该关掉它。
|
|
395
|
+
'<button id="orb" type="button" aria-label="Agent 任务"><span class="orb-ring"></span>'
|
|
396
|
+
+ '<svg class="orb-mark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.25" y="4.75" width="17.5" height="14.5" rx="3.25"></rect><path d="M3.25 9.4h17.5"></path><circle cx="6.15" cy="7.05" r=".9" fill="currentColor" stroke="none"></circle><circle cx="8.75" cy="7.05" r=".9" fill="currentColor" stroke="none"></circle></svg>'
|
|
397
|
+
+ '<span class="orb-badge"></span></button>',
|
|
398
|
+
'<div id="orbCard" role="menu" aria-label="Agent 任务"><div class="orb-head"><span>Agent 任务</span><span class="orb-task"></span></div><div class="orb-list"></div><div class="orb-foot"></div></div>',
|
|
367
399
|
].join('\n')
|
|
368
400
|
|
|
369
401
|
return [
|
|
@@ -424,10 +456,9 @@ export function buildPageChromeScript(bindingToken = '', surface: ChromeSurface
|
|
|
424
456
|
" // 根元素高度锁成视口高、padding 算在内部,滚动条就只由真实内容决定。",
|
|
425
457
|
" // 书签栏开着时顶栏会多 34px,下移量要跟着变(否则页面会被压住一截)。",
|
|
426
458
|
" let bookmarksVisible = false",
|
|
427
|
-
" // page
|
|
428
|
-
"
|
|
429
|
-
"
|
|
430
|
-
" : 84 + (bookmarksVisible ? 34 : 0)",
|
|
459
|
+
" // page 表面上工具栏和书签栏都画在 frame 视图里,页面视图本身就从它们下面开始,",
|
|
460
|
+
" // 所以页面一点也不用让位(以前要让 34px,position:fixed 的头部于是被压住)。",
|
|
461
|
+
" const chromeInset = () => CHROME_SURFACE === 'page' ? 0 : 84 + (bookmarksVisible ? 34 : 0)",
|
|
431
462
|
" // 下移量。**过渡只在第二次应用之后才补上**:chrome 每次导航都会重新注入,第一次就带",
|
|
432
463
|
" // 过渡的话,每个页面加载(书签栏开着时 0 -> 34px)都会看到内容自己往下滑一下。",
|
|
433
464
|
" let insetApplied = false",
|
|
@@ -458,6 +489,200 @@ export function buildPageChromeScript(bindingToken = '', surface: ChromeSurface
|
|
|
458
489
|
" const tasksBtn = root.getElementById('tasksBtn')",
|
|
459
490
|
" const taskPanel = root.getElementById('taskPanel')",
|
|
460
491
|
" const taskList = root.getElementById('taskList')",
|
|
492
|
+
" const orb = root.getElementById('orb')",
|
|
493
|
+
" const orbCard = root.getElementById('orbCard')",
|
|
494
|
+
"",
|
|
495
|
+
" // ── 悬浮球 ──────────────────────────────────────────────────────────────",
|
|
496
|
+
" // 两个数据源:任务摘要(status / latestAction)与定向的 task.todos 补丁",
|
|
497
|
+
" // (Agent 的 todo 清单)。清单**不进摘要** —— 摘要会被注入每个访问过的页面的主世界。",
|
|
498
|
+
" const taskTodosCache = new Map()",
|
|
499
|
+
" const orbBadge = orb instanceof HTMLElement ? orb.querySelector('.orb-badge') : null",
|
|
500
|
+
" const orbCardTask = orbCard instanceof HTMLElement ? orbCard.querySelector('.orb-task') : null",
|
|
501
|
+
" const orbCardList = orbCard instanceof HTMLElement ? orbCard.querySelector('.orb-list') : null",
|
|
502
|
+
" const orbCardFoot = orbCard instanceof HTMLElement ? orbCard.querySelector('.orb-foot') : null",
|
|
503
|
+
" let orbCardTimer = null",
|
|
504
|
+
" const orbTask = () => {",
|
|
505
|
+
" const list = Array.isArray(window.__dshTasks) ? window.__dshTasks : []",
|
|
506
|
+
" return list.find(task => task && task.key === chromeSelectedTaskKey) || list.find(task => task && task.active === true) || list[0]",
|
|
507
|
+
" }",
|
|
508
|
+
" let orbRenderSignature = null",
|
|
509
|
+
" const renderOrb = () => {",
|
|
510
|
+
" if (!(orb instanceof HTMLElement)) return",
|
|
511
|
+
" const task = orbTask()",
|
|
512
|
+
" const key = task && typeof task.key === 'string' ? task.key : ''",
|
|
513
|
+
" const todos = key !== '' && Array.isArray(taskTodosCache.get(key)) ? taskTodosCache.get(key) : []",
|
|
514
|
+
" const status = task && typeof task.status === 'string' ? task.status : 'idle'",
|
|
515
|
+
" // 清单整表重建会把人的滚动位置弹回顶部、让转圈动画从头开始 —— 内容没变就整段跳过。",
|
|
516
|
+
" const signature = key + '\u0001' + status + '\u0001' + (task && task.latestAction ? task.latestAction : '') + '\u0001' + todos.map(item => (item && item.status) + ':' + (item && item.content)).join('\u0002')",
|
|
517
|
+
" if (signature === orbRenderSignature) return",
|
|
518
|
+
" orbRenderSignature = signature",
|
|
519
|
+
" const running = todos.some(item => item && item.status === 'in_progress')",
|
|
520
|
+
" // 终态优先:Agent 的清单里通常有一条 in_progress,若让 running 盖过 failed/waiting-user,",
|
|
521
|
+
" // 失败和「等你操作」就永远看不到(球一直转圈)。",
|
|
522
|
+
" const settled = status === 'failed' || status === 'waiting-user'",
|
|
523
|
+
" const working = status === 'running' || (running && !settled)",
|
|
524
|
+
" const completed = todos.filter(item => item && item.status === 'completed').length",
|
|
525
|
+
" // 清单被截断到上限时(与桥接 normalize 的 40 一致),最后那些可能没送进来 ——",
|
|
526
|
+
" // 不能凭前 40 条就宣布「全部完成」。",
|
|
527
|
+
" const capped = todos.length >= 40",
|
|
528
|
+
" const allDone = todos.length > 0 && completed === todos.length && !capped",
|
|
529
|
+
" orb.classList.toggle('working', working)",
|
|
530
|
+
" orb.classList.toggle('done', !working && allDone)",
|
|
531
|
+
" orb.classList.toggle('failed', !working && status === 'failed')",
|
|
532
|
+
" orb.classList.toggle('waiting', !working && status === 'waiting-user')",
|
|
533
|
+
" if (orbBadge) {",
|
|
534
|
+
" // 角标只在有话要说时出现:进度、完成、失败、等你。空闲就是干净的一个球。",
|
|
535
|
+
" if (working) orbBadge.textContent = todos.length > 0 ? String(completed) + '/' + String(todos.length) + (capped ? '+' : '') : ''",
|
|
536
|
+
" else if (status === 'failed') orbBadge.textContent = '!'",
|
|
537
|
+
" else if (status === 'waiting-user') orbBadge.textContent = '‖'",
|
|
538
|
+
" else if (allDone) orbBadge.textContent = '✓'",
|
|
539
|
+
" else orbBadge.textContent = ''",
|
|
540
|
+
" }",
|
|
541
|
+
" // 没有 label 时**不要**退回原始 key(session-6012f8c2-… 是内部键,给人看的东西)。",
|
|
542
|
+
" if (orbCardTask) orbCardTask.textContent = task && typeof task.label === 'string' ? task.label : ''",
|
|
543
|
+
" if (orbCardList) {",
|
|
544
|
+
" orbCardList.textContent = ''",
|
|
545
|
+
" if (todos.length === 0) {",
|
|
546
|
+
" const empty = document.createElement('div')",
|
|
547
|
+
" empty.className = 'orb-empty'",
|
|
548
|
+
" empty.textContent = working ? '正在执行浏览器操作…' : '暂无任务清单'",
|
|
549
|
+
" orbCardList.append(empty)",
|
|
550
|
+
" } else {",
|
|
551
|
+
" for (const item of todos) {",
|
|
552
|
+
" const state = item && item.status === 'completed' ? 'completed' : item && item.status === 'in_progress' ? 'in_progress' : 'pending'",
|
|
553
|
+
" const row = document.createElement('div')",
|
|
554
|
+
" row.className = 'orb-item ' + state",
|
|
555
|
+
" const mark = document.createElement('span')",
|
|
556
|
+
" mark.className = 'orb-mark'",
|
|
557
|
+
" if (state === 'in_progress') { const spin = document.createElement('span'); spin.className = 'orb-spin'; mark.append(spin) }",
|
|
558
|
+
" else mark.textContent = state === 'completed' ? '✓' : '○'",
|
|
559
|
+
" const text = document.createElement('span')",
|
|
560
|
+
" text.className = 'orb-text'",
|
|
561
|
+
" text.textContent = item && typeof item.content === 'string' ? item.content : ''",
|
|
562
|
+
" row.append(mark, text)",
|
|
563
|
+
" orbCardList.append(row)",
|
|
564
|
+
" }",
|
|
565
|
+
" }",
|
|
566
|
+
" }",
|
|
567
|
+
" if (orbCardFoot) {",
|
|
568
|
+
" const latest = task && typeof task.latestAction === 'string' ? task.latestAction : ''",
|
|
569
|
+
" orbCardFoot.textContent = working && latest !== '' ? '现在执行 · ' + latest : allDone ? '全部完成' : status === 'waiting-user' ? '等待你操作' : status === 'failed' ? '上次执行失败' : ''",
|
|
570
|
+
" orbCardFoot.style.display = orbCardFoot.textContent === '' ? 'none' : ''",
|
|
571
|
+
" }",
|
|
572
|
+
" // 列表重建后卡片高度可能变了(缩略图补丁 5Hz 也会走到这里)—— 开着的话重新贴一次球,",
|
|
573
|
+
" // 否则卡片长高会往下压住球(c1f6291 修的那个现象换了个触发路径)。",
|
|
574
|
+
" try { if (orbCard instanceof HTMLElement && orbCard.classList.contains('open')) placeOrbCard() } catch { /* placeOrbCard 还没定义 */ }",
|
|
575
|
+
" }",
|
|
576
|
+
" // 悬浮球可拖动,位置交给宿主保存(chrome-prefs.json)—— 存在页面里会按源丢。",
|
|
577
|
+
" let orbPos = null",
|
|
578
|
+
" let orbDraggedAt = 0",
|
|
579
|
+
" // 悬浮球**不随页面缩放变大变小**:它的大小是本地 px,宿主那层 zoom=1/缩放 正好抵消掉",
|
|
580
|
+
" // (实测 100% 与 130% 下屏幕上都是 44px)。但坐标有两套 —— 事件与 rect 是页面 px,",
|
|
581
|
+
" // style.left/top 是本地 px —— 混用就会拖动跳、夹取偏、卡片错位。",
|
|
582
|
+
" const orbScale = () => { try { return typeof chromeScale === 'function' ? chromeScale() : 1 } catch { return 1 } }",
|
|
583
|
+
" const clampOrbPos = (x, y) => {",
|
|
584
|
+
" const s = orbScale()",
|
|
585
|
+
" const maxX = Math.max(8, window.innerWidth * s - (orb instanceof HTMLElement ? orb.offsetWidth : 44) - 8)",
|
|
586
|
+
" const maxY = Math.max(8, window.innerHeight * s - (orb instanceof HTMLElement ? orb.offsetHeight : 44) - 8)",
|
|
587
|
+
" return { x: Math.min(Math.max(8, x), maxX), y: Math.min(Math.max(8, y), maxY) }",
|
|
588
|
+
" }",
|
|
589
|
+
" const applyOrbPos = () => {",
|
|
590
|
+
" if (!(orb instanceof HTMLElement)) return",
|
|
591
|
+
" if (orbPos === null) { orb.style.left = ''; orb.style.top = ''; orb.style.right = ''; orb.style.bottom = ''; return }",
|
|
592
|
+
" // 存下来的坐标是在**当时那个窗口**里量出来的:窗口变小、换显示器、恢复最大化之后",
|
|
593
|
+
" // 它可能已经在屏幕外 —— 那就再也拖不回来了(而且位置还会一直存在 chrome-prefs.json 里)。",
|
|
594
|
+
" orbPos = clampOrbPos(orbPos.x, orbPos.y)",
|
|
595
|
+
" orb.style.left = Math.round(orbPos.x) + 'px'",
|
|
596
|
+
" orb.style.top = Math.round(orbPos.y) + 'px'",
|
|
597
|
+
" orb.style.right = 'auto'",
|
|
598
|
+
" orb.style.bottom = 'auto'",
|
|
599
|
+
" }",
|
|
600
|
+
" const emitOrbMove = (x, y) => {",
|
|
601
|
+
" if (bindingToken === '') return",
|
|
602
|
+
" try { const binding = window.__dshBrowserTaskAction; if (typeof binding === 'function') binding(JSON.stringify({ type: 'orb-move', x, y, token: bindingToken })) } catch { /* host unavailable */ }",
|
|
603
|
+
" }",
|
|
604
|
+
" let orbDrag = null",
|
|
605
|
+
" if (orb instanceof HTMLElement) {",
|
|
606
|
+
" orb.addEventListener('pointerdown', event => {",
|
|
607
|
+
" if (event.button !== 0) return",
|
|
608
|
+
" // moved 要比「按下时的起点」:拿 orb.offsetLeft 比的话,它是上一帧刚被 applyOrbPos 写进去的值,",
|
|
609
|
+
" // 于是慢速拖动(每帧 <3px)永远不算拖动 —— 位置不落盘,松手还会误触 click。",
|
|
610
|
+
" const s = orbScale()",
|
|
611
|
+
" orbDrag = { startX: event.clientX, startY: event.clientY, localX: event.clientX * s, localY: event.clientY * s, dx: event.clientX * s - orb.offsetLeft, dy: event.clientY * s - orb.offsetTop, moved: false, pressed: false }",
|
|
612
|
+
" orb.classList.add('dragging')",
|
|
613
|
+
" // 不用 setPointerCapture:窗口级的 pointermove/pointerup 已经够了,而捕获会把后续的",
|
|
614
|
+
" // click 重定向到球本身(标签拖动那条同款教训)。",
|
|
615
|
+
" })",
|
|
616
|
+
" window.addEventListener('pointermove', event => {",
|
|
617
|
+
" // 在窗口外松手收不到 pointerup(pointercancel 也不保证)—— 不看 buttons 的话,",
|
|
618
|
+
" // 鼠标一回到页面,球就会在没按任何键的情况下跟着光标跑。",
|
|
619
|
+
" // 但**不能**见 0 就收尾:合成输入(离屏测试、CDP)的 pointermove 常常不带 buttons,",
|
|
620
|
+
" // 那会把正常拖动也打断。所以只在「这次拖动曾经观察到按下」之后才认这个信号。",
|
|
621
|
+
" if (orbDrag === null) return",
|
|
622
|
+
" if (event.buttons !== 0) orbDrag.pressed = true",
|
|
623
|
+
" else if (orbDrag.pressed) { endOrbDrag(); return }",
|
|
624
|
+
" const s = orbScale()",
|
|
625
|
+
" const spot = clampOrbPos(event.clientX * s - orbDrag.dx, event.clientY * s - orbDrag.dy)",
|
|
626
|
+
" const x = spot.x",
|
|
627
|
+
" const y = spot.y",
|
|
628
|
+
" if (Math.abs(event.clientX * s - orbDrag.localX) > 3 || Math.abs(event.clientY * s - orbDrag.localY) > 3) orbDrag.moved = true",
|
|
629
|
+
" orbPos = { x, y }",
|
|
630
|
+
" applyOrbPos()",
|
|
631
|
+
" if (orbDrag.moved && orbCard instanceof HTMLElement) orbCard.classList.remove('open')",
|
|
632
|
+
" })",
|
|
633
|
+
" const endOrbDrag = () => {",
|
|
634
|
+
" if (orbDrag === null) return",
|
|
635
|
+
" const moved = orbDrag.moved",
|
|
636
|
+
" orbDrag = null",
|
|
637
|
+
" orb.classList.remove('dragging')",
|
|
638
|
+
" if (!moved) return",
|
|
639
|
+
" orbDraggedAt = Date.now()",
|
|
640
|
+
" if (orbPos !== null) emitOrbMove(orbPos.x, orbPos.y)",
|
|
641
|
+
" }",
|
|
642
|
+
" window.addEventListener('pointerup', endOrbDrag)",
|
|
643
|
+
" window.addEventListener('pointercancel', endOrbDrag)",
|
|
644
|
+
" // 在别的窗口/桌面上松手时,失焦是最可靠的信号(那时连 pointerup 都收不到)。",
|
|
645
|
+
" window.addEventListener('blur', endOrbDrag)",
|
|
646
|
+
" // 窗口一变形:已存的坐标要重新夹一次;卡片开着的话它也要跟着球重新定位。",
|
|
647
|
+
" window.addEventListener('resize', () => { applyOrbPos(); if (orbCard instanceof HTMLElement && orbCard.classList.contains('open')) placeOrbCard() })",
|
|
648
|
+
" }",
|
|
649
|
+
" const placeOrbCard = () => {",
|
|
650
|
+
" if (!(orb instanceof HTMLElement) || !(orbCard instanceof HTMLElement)) return",
|
|
651
|
+
" const s = orbScale()",
|
|
652
|
+
" const vw = window.innerWidth * s",
|
|
653
|
+
" const vh = window.innerHeight * s",
|
|
654
|
+
" // 视口比卡片还矮/还窄时(小窗口、分屏),先把卡片压到放得下再量 —— 否则它会",
|
|
655
|
+
" // 被夹在 8..vh-8 之间,正好盖住贴底的球(review 报的 #10)。",
|
|
656
|
+
" orbCard.style.maxHeight = Math.max(120, vh - 24) + 'px'",
|
|
657
|
+
" orbCard.style.width = Math.max(160, Math.min(308, vw - 16)) + 'px'",
|
|
658
|
+
" const box = orb.getBoundingClientRect()",
|
|
659
|
+
" const width = orbCard.offsetWidth > 0 ? orbCard.offsetWidth : 308",
|
|
660
|
+
" const height = orbCard.offsetHeight",
|
|
661
|
+
" const left = Math.min(Math.max(8, box.left * s + (box.width * s) / 2 - width / 2), Math.max(8, window.innerWidth * s - width - 8))",
|
|
662
|
+
" // 球拖到上方时,卡片再往上放就会被夹到视口顶、**盖住球自己** —— 上面放不下就翻到下面。",
|
|
663
|
+
" const gap = 8",
|
|
664
|
+
" const above = box.top * s - gap - height",
|
|
665
|
+
" const top = above >= gap ? above : Math.min(box.bottom * s + gap, Math.max(gap, window.innerHeight * s - height - gap))",
|
|
666
|
+
" orbCard.style.left = Math.round(left) + 'px'",
|
|
667
|
+
" orbCard.style.top = Math.round(top) + 'px'",
|
|
668
|
+
" orbCard.style.right = 'auto'",
|
|
669
|
+
" orbCard.style.bottom = 'auto'",
|
|
670
|
+
" }",
|
|
671
|
+
" const openOrbCard = () => {",
|
|
672
|
+
" if (orbCardTimer !== null) { clearTimeout(orbCardTimer); orbCardTimer = null }",
|
|
673
|
+
" if (orbCard instanceof HTMLElement) { orbCard.classList.add('open'); placeOrbCard() }",
|
|
674
|
+
" }",
|
|
675
|
+
" const closeOrbCardSoon = () => {",
|
|
676
|
+
" if (orbCardTimer !== null) clearTimeout(orbCardTimer)",
|
|
677
|
+
" orbCardTimer = setTimeout(() => { if (orbCard instanceof HTMLElement) orbCard.classList.remove('open') }, 150)",
|
|
678
|
+
" }",
|
|
679
|
+
" if (orb instanceof HTMLElement && orbCard instanceof HTMLElement) {",
|
|
680
|
+
" orb.addEventListener('pointerenter', openOrbCard)",
|
|
681
|
+
" orb.addEventListener('pointerleave', closeOrbCardSoon)",
|
|
682
|
+
" orbCard.addEventListener('pointerenter', openOrbCard)",
|
|
683
|
+
" orbCard.addEventListener('pointerleave', closeOrbCardSoon)",
|
|
684
|
+
" orb.addEventListener('click', () => { if (Date.now() - orbDraggedAt < 350) return; renderOrb(); if (orbCard.classList.contains('open')) orbCard.classList.remove('open'); else openOrbCard() })",
|
|
685
|
+
" }",
|
|
461
686
|
" const taskControl = root.getElementById('taskControl')",
|
|
462
687
|
" const taskClose = root.getElementById('taskClose')",
|
|
463
688
|
" const trailList = root.getElementById('trailList')",
|
|
@@ -1193,7 +1418,7 @@ export function buildPageChromeScript(bindingToken = '', surface: ChromeSurface
|
|
|
1193
1418
|
" row.append(thumb, title, meta, recent, failure)",
|
|
1194
1419
|
" // 复用之后闭包里的 key 会过期 —— 点的时候从 dataset 现读(第 46 轮的教训)。",
|
|
1195
1420
|
" row.addEventListener('click', () => {",
|
|
1196
|
-
" const key = row.dataset.
|
|
1421
|
+
" const key = row.dataset.dshKey",
|
|
1197
1422
|
" if (typeof key === 'string' && key !== '') emitTaskAction(key)",
|
|
1198
1423
|
" })",
|
|
1199
1424
|
" return row",
|
|
@@ -1221,14 +1446,15 @@ export function buildPageChromeScript(bindingToken = '', surface: ChromeSurface
|
|
|
1221
1446
|
" window.__dshTaskRender = renderTasks",
|
|
1222
1447
|
" const patchTaskRow = task => {",
|
|
1223
1448
|
" if (!task || typeof task !== 'object' || typeof task.key !== 'string') return false",
|
|
1224
|
-
" const row = [...taskList.children].find(candidate => candidate instanceof HTMLElement && candidate.dataset.
|
|
1449
|
+
" const row = [...taskList.children].find(candidate => candidate instanceof HTMLElement && candidate.dataset.dshKey === task.key)",
|
|
1225
1450
|
" if (!(row instanceof HTMLButtonElement)) return false",
|
|
1226
1451
|
" const version = String(Number.isFinite(task.thumbnailVersion) ? task.thumbnailVersion : 0)",
|
|
1227
1452
|
" if (row.dataset.dshThumbnailVersion !== version) return false",
|
|
1228
1453
|
" const status = ['idle', 'running', 'waiting-user', 'failed'].includes(task.status) ? task.status : 'idle'",
|
|
1229
1454
|
" const control = task.control === 'human' ? 'human' : 'agent'",
|
|
1230
1455
|
" row.className = 'task-row status-' + status + (task.active ? ' active' : '')",
|
|
1231
|
-
"
|
|
1456
|
+
" // 与整表渲染同一条规则:当前任务的行点不动(点了也只是切到自己)。",
|
|
1457
|
+
" row.disabled = task.active === true",
|
|
1232
1458
|
" const state = row.querySelector('[data-dsh-task-state]')",
|
|
1233
1459
|
" if (state instanceof HTMLElement) state.setAttribute('aria-label', statusText(status) + (task.active ? ',当前任务' : ',后台任务'))",
|
|
1234
1460
|
" const label = row.querySelector('[data-dsh-task-label]')",
|
|
@@ -1577,7 +1803,11 @@ export function buildPageChromeScript(bindingToken = '', surface: ChromeSurface
|
|
|
1577
1803
|
" for (const key of [...taskThumbCache.keys()]) if (!liveKeys.has(key)) taskThumbCache.delete(key)",
|
|
1578
1804
|
" window.__dshTrail = Array.isArray(message.trail) ? message.trail : []",
|
|
1579
1805
|
" window.__dshWorkspacePanels = message.panels && typeof message.panels === 'object' ? message.panels : { tasks: false, trail: false }",
|
|
1806
|
+
" const saved = message.orbPosition",
|
|
1807
|
+
" orbPos = saved && typeof saved.x === 'number' && typeof saved.y === 'number' ? { x: saved.x, y: saved.y } : null",
|
|
1808
|
+
" applyOrbPos()",
|
|
1580
1809
|
" renderTasks()",
|
|
1810
|
+
" renderOrb()",
|
|
1581
1811
|
" renderTabs()",
|
|
1582
1812
|
" renderTrail()",
|
|
1583
1813
|
" applyWorkspacePanels()",
|
|
@@ -1611,6 +1841,10 @@ export function buildPageChromeScript(bindingToken = '', surface: ChromeSurface
|
|
|
1611
1841
|
" if (typeof operation.dataUrl === 'string') taskThumbCache.set(operation.key, operation.dataUrl)",
|
|
1612
1842
|
" const task = tasks.find(candidate => candidate && candidate.key === operation.key)",
|
|
1613
1843
|
" if (task) { task.thumbnailVersion = operation.version; fullTaskRender = true }",
|
|
1844
|
+
" } else if (operation.op === 'task.todos' && typeof operation.key === 'string') {",
|
|
1845
|
+
" // Agent 的任务清单:定向补丁(不进摘要),悬浮球读它。",
|
|
1846
|
+
" taskTodosCache.set(operation.key, Array.isArray(operation.todos) ? operation.todos : [])",
|
|
1847
|
+
" renderOrb()",
|
|
1614
1848
|
" } else if (operation.op === 'trail.append' && operation.taskKey === chromeSelectedTaskKey && operation.entry) {",
|
|
1615
1849
|
" const trail = Array.isArray(window.__dshTrail) ? window.__dshTrail : []",
|
|
1616
1850
|
" window.__dshTrail = [...trail, operation.entry]",
|
|
@@ -1656,6 +1890,7 @@ export function buildPageChromeScript(bindingToken = '', surface: ChromeSurface
|
|
|
1656
1890
|
" window.__dshTasks = tasks",
|
|
1657
1891
|
" if (fullTaskRender) renderTasks()",
|
|
1658
1892
|
" else if (taskPatches.length > 0) { renderTaskControl(tasks); for (const task of taskPatches) { if (!patchTaskRow(task)) { renderTasks(); break } } }",
|
|
1893
|
+
" renderOrb()",
|
|
1659
1894
|
" if (trailReplaced) renderTrail()",
|
|
1660
1895
|
" else for (const entry of trailAppends) appendTrailEntry(entry)",
|
|
1661
1896
|
" // 换页也要重算星标:宿主给的标签 url 是**规范化过**的(没有结尾斜杠),而收藏里",
|