@restregular/dsh-visual-browser 0.2.0 → 0.2.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/README.md +6 -1
- package/client.js +210 -18
- package/package.json +1 -1
- package/test/package.test.mjs +2 -0
package/README.md
CHANGED
|
@@ -12,7 +12,8 @@ A DeepSeek Harness plugin that gives the Agent a **real Chrome** and gives you a
|
|
|
12
12
|
animation-frame batches: click, drag, scroll and type with normal latency
|
|
13
13
|
instead of "screenshot after the fact" latency.
|
|
14
14
|
- Every action — the Agent's and yours — lands in a collapsible, scrollable
|
|
15
|
-
**operation log
|
|
15
|
+
**operation log** whose height you can drag. Records only: there is nothing to
|
|
16
|
+
replay.
|
|
16
17
|
- Cookies persist across restarts (one dedicated Chrome profile), so a login the
|
|
17
18
|
Agent performs once keeps working.
|
|
18
19
|
|
|
@@ -40,6 +41,10 @@ dependencies; it talks CDP over the built-in WebSocket).
|
|
|
40
41
|
|
|
41
42
|
The operation log at the bottom remembers the Agent's actions and yours. Click
|
|
42
43
|
its header to fold it away, or scroll it when the list is longer than the pane.
|
|
44
|
+
Drag the handle above the log to choose how much room it takes (the picture keeps
|
|
45
|
+
a minimum reserve); double-click the handle to go back to the default, or focus
|
|
46
|
+
it and use ↑ / ↓ (Shift for bigger steps, Home / End for the extremes). The
|
|
47
|
+
chosen height is remembered per browser.
|
|
43
48
|
|
|
44
49
|
### Dark mode
|
|
45
50
|
|
package/client.js
CHANGED
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
'timeline.empty': '还没有操作。Agent 的点击、输入与你自己的操作都会记录在这里。',
|
|
48
48
|
'timeline.count': '条',
|
|
49
49
|
'timeline.expand': '展开操作记录',
|
|
50
|
+
'timeline.resize': '上下拖动调整操作记录高度,双击复位',
|
|
50
51
|
'timeline.collapse': '折叠操作记录',
|
|
51
52
|
'error.offline': '连不上宿主路由(/visual-browser/api),请确认插件已启用且宿主仍在运行。',
|
|
52
53
|
'error.frame': '画面流中断,正在重连…',
|
|
@@ -100,6 +101,7 @@
|
|
|
100
101
|
'timeline.empty': 'No actions yet. Agent clicks, typing, and your own gestures are listed here.',
|
|
101
102
|
'timeline.count': 'items',
|
|
102
103
|
'timeline.expand': 'Expand the action log',
|
|
104
|
+
'timeline.resize': 'Drag up or down to resize the action log; double-click to reset',
|
|
103
105
|
'timeline.collapse': 'Collapse the action log',
|
|
104
106
|
'error.offline': 'Cannot reach the host route (/visual-browser/api). Check that the plugin is enabled and the host is running.',
|
|
105
107
|
'error.frame': 'The picture stream dropped; reconnecting…',
|
|
@@ -170,6 +172,11 @@
|
|
|
170
172
|
'.dvb-log-title{font-weight:600;color:var(--dsw-alias-label-secondary)}',
|
|
171
173
|
'.dvb-log-count{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary)}',
|
|
172
174
|
'.dvb-log-body{overflow-y:auto;overscroll-behavior:contain;max-height:min(44vh,320px);min-height:0;padding:0 6px 8px;display:flex;flex-direction:column;gap:1px}',
|
|
175
|
+
'.dvb-log[data-sized="true"] .dvb-log-body{max-height:none;height:var(--dvb-log-h,200px)}',
|
|
176
|
+
'.dvb-split{flex:none;display:flex;align-items:center;justify-content:center;height:11px;margin:-1px 0;padding:0;border:0;background:transparent;cursor:ns-resize;touch-action:none}',
|
|
177
|
+
'.dvb-split::before{content:"";width:36px;height:3px;border-radius:2px;background:var(--dsw-alias-border-l2);transition:background .12s ease}',
|
|
178
|
+
'.dvb-split:hover::before,.dvb-split[data-active="true"]::before{background:var(--dsw-alias-brand-primary)}',
|
|
179
|
+
'.dvb-split:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary));outline-offset:-3px;border-radius:6px}',
|
|
173
180
|
'.dvb-log-empty{padding:3px 6px 10px;font-size:11.5px;color:var(--dsw-alias-label-tertiary)}',
|
|
174
181
|
'.dvb-row{display:flex;align-items:baseline;gap:6px;padding:4px 6px;border-radius:6px;font-size:12px;min-width:0}',
|
|
175
182
|
'.dvb-row:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
@@ -550,6 +557,34 @@
|
|
|
550
557
|
// Tab body
|
|
551
558
|
// -------------------------------------------------------------------
|
|
552
559
|
|
|
560
|
+
/**
|
|
561
|
+
* Height of the resizable action log. The panel keeps the choice in
|
|
562
|
+
* localStorage so a chosen split survives reopening the tab.
|
|
563
|
+
*/
|
|
564
|
+
var LOG_HEIGHT_KEY = 'dsh.visual-browser.log-height.v1';
|
|
565
|
+
var LOG_MIN_HEIGHT = 96;
|
|
566
|
+
var LOG_STAGE_RESERVE = 220;
|
|
567
|
+
|
|
568
|
+
function readStoredLogHeight() {
|
|
569
|
+
try {
|
|
570
|
+
var raw = window.localStorage.getItem(LOG_HEIGHT_KEY);
|
|
571
|
+
var value = Number(raw);
|
|
572
|
+
if (!Number.isFinite(value) || value < LOG_MIN_HEIGHT) return null;
|
|
573
|
+
return Math.round(value);
|
|
574
|
+
} catch {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function storeLogHeight(value) {
|
|
580
|
+
try {
|
|
581
|
+
if (value === null) window.localStorage.removeItem(LOG_HEIGHT_KEY);
|
|
582
|
+
else window.localStorage.setItem(LOG_HEIGHT_KEY, String(Math.round(value)));
|
|
583
|
+
} catch {
|
|
584
|
+
/* storage can be unavailable; the choice just does not persist */
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
553
588
|
var EMPTY_STATE = {
|
|
554
589
|
running: false,
|
|
555
590
|
starting: false,
|
|
@@ -591,11 +626,17 @@
|
|
|
591
626
|
var [draft, setDraft] = useState('');
|
|
592
627
|
var [dirty, setDirty] = useState(false);
|
|
593
628
|
var [logOpen, setLogOpen] = useState(true);
|
|
629
|
+
var [logHeight, setLogHeight] = useState(readStoredLogHeight);
|
|
630
|
+
var [resizing, setResizing] = useState(false);
|
|
594
631
|
var [live, setLive] = useState(false);
|
|
595
632
|
var [streamRetry, setStreamRetry] = useState(0);
|
|
596
633
|
var [retrying, setRetrying] = useState(false);
|
|
597
634
|
var [focused, setFocused] = useState(false);
|
|
598
635
|
|
|
636
|
+
var rootRef = useRef(null);
|
|
637
|
+
var logRef = useRef(null);
|
|
638
|
+
var logBodyRef = useRef(null);
|
|
639
|
+
var splitRef = useRef(null);
|
|
599
640
|
var stageRef = useRef(null);
|
|
600
641
|
var canvasRef = useRef(null);
|
|
601
642
|
var liveRef = useRef(false);
|
|
@@ -1081,6 +1122,133 @@
|
|
|
1081
1122
|
});
|
|
1082
1123
|
};
|
|
1083
1124
|
|
|
1125
|
+
// Keeps a stored split inside the panel: the picture always keeps its
|
|
1126
|
+
// reserve, however the pane is resized later.
|
|
1127
|
+
useEffect(
|
|
1128
|
+
function () {
|
|
1129
|
+
if (logHeight === null) return undefined;
|
|
1130
|
+
var root = rootRef.current;
|
|
1131
|
+
if (!root) return undefined;
|
|
1132
|
+
var clampToPanel = function () {
|
|
1133
|
+
var limit = Math.max(LOG_MIN_HEIGHT, root.clientHeight - LOG_STAGE_RESERVE);
|
|
1134
|
+
if (logHeight > limit) {
|
|
1135
|
+
setLogHeight(limit);
|
|
1136
|
+
storeLogHeight(limit);
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
clampToPanel();
|
|
1140
|
+
if (typeof ResizeObserver !== 'function') return undefined;
|
|
1141
|
+
var observer = new ResizeObserver(clampToPanel);
|
|
1142
|
+
observer.observe(root);
|
|
1143
|
+
return function () {
|
|
1144
|
+
observer.disconnect();
|
|
1145
|
+
};
|
|
1146
|
+
},
|
|
1147
|
+
[logHeight, logOpen],
|
|
1148
|
+
);
|
|
1149
|
+
|
|
1150
|
+
var applyLogHeight = function (value) {
|
|
1151
|
+
var log = logRef.current;
|
|
1152
|
+
if (!log) return;
|
|
1153
|
+
if (value === null) {
|
|
1154
|
+
log.removeAttribute('data-sized');
|
|
1155
|
+
log.style.removeProperty('--dvb-log-h');
|
|
1156
|
+
} else {
|
|
1157
|
+
log.setAttribute('data-sized', 'true');
|
|
1158
|
+
log.style.setProperty('--dvb-log-h', Math.round(value) + 'px');
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
var commitLogHeight = function (value) {
|
|
1163
|
+
setLogHeight(value);
|
|
1164
|
+
storeLogHeight(value);
|
|
1165
|
+
applyLogHeight(value);
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
// The CSS `height` is a content-box height; measuring the same box keeps
|
|
1169
|
+
// a drag from jumping by the body's padding on the first pixel.
|
|
1170
|
+
var currentLogHeight = function () {
|
|
1171
|
+
var body = logBodyRef.current;
|
|
1172
|
+
if (!body) return LOG_MIN_HEIGHT;
|
|
1173
|
+
var computed = window.getComputedStyle(body).height;
|
|
1174
|
+
var value = Number(String(computed).replace('px', ''));
|
|
1175
|
+
return Number.isFinite(value) && value > 0 ? value : body.getBoundingClientRect().height;
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
var logHeightLimit = function () {
|
|
1179
|
+
var root = rootRef.current;
|
|
1180
|
+
if (!root) return 420;
|
|
1181
|
+
return Math.max(LOG_MIN_HEIGHT, root.clientHeight - LOG_STAGE_RESERVE);
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
// Drag the divider between the picture and the log.
|
|
1185
|
+
var onSplitDown = function (event) {
|
|
1186
|
+
if (event.button !== undefined && event.button !== 0) return;
|
|
1187
|
+
event.preventDefault();
|
|
1188
|
+
event.stopPropagation();
|
|
1189
|
+
var startHeight = currentLogHeight();
|
|
1190
|
+
var startY = event.clientY;
|
|
1191
|
+
var pointerId = event.pointerId;
|
|
1192
|
+
var handle = event.currentTarget;
|
|
1193
|
+
var active = true;
|
|
1194
|
+
setResizing(true);
|
|
1195
|
+
try {
|
|
1196
|
+
if (handle.setPointerCapture) handle.setPointerCapture(pointerId);
|
|
1197
|
+
} catch {
|
|
1198
|
+
/* capture is a nicety */
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
var move = function (moveEvent) {
|
|
1202
|
+
if (!active) return;
|
|
1203
|
+
var limit = logHeightLimit();
|
|
1204
|
+
var next = Math.min(limit, Math.max(LOG_MIN_HEIGHT, Math.round(startHeight + (startY - moveEvent.clientY))));
|
|
1205
|
+
applyLogHeight(next);
|
|
1206
|
+
};
|
|
1207
|
+
var finish = function () {
|
|
1208
|
+
if (!active) return;
|
|
1209
|
+
active = false;
|
|
1210
|
+
handle.removeEventListener('pointermove', move);
|
|
1211
|
+
handle.removeEventListener('pointerup', finish);
|
|
1212
|
+
handle.removeEventListener('pointercancel', finish);
|
|
1213
|
+
try {
|
|
1214
|
+
if (handle.releasePointerCapture) handle.releasePointerCapture(pointerId);
|
|
1215
|
+
} catch {
|
|
1216
|
+
/* already released */
|
|
1217
|
+
}
|
|
1218
|
+
setResizing(false);
|
|
1219
|
+
var log = logRef.current;
|
|
1220
|
+
var value = log ? Number(String(log.style.getPropertyValue('--dvb-log-h')).replace('px', '')) : NaN;
|
|
1221
|
+
if (Number.isFinite(value) && value > 0) commitLogHeight(Math.round(value));
|
|
1222
|
+
else commitLogHeight(null);
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
handle.addEventListener('pointermove', move);
|
|
1226
|
+
handle.addEventListener('pointerup', finish);
|
|
1227
|
+
handle.addEventListener('pointercancel', finish);
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1230
|
+
var onSplitKeyDown = function (event) {
|
|
1231
|
+
var step = event.shiftKey ? 64 : 24;
|
|
1232
|
+
var current = currentLogHeight();
|
|
1233
|
+
if (event.key === 'ArrowUp') {
|
|
1234
|
+
event.preventDefault();
|
|
1235
|
+
commitLogHeight(Math.min(logHeightLimit(), Math.round(current + step)));
|
|
1236
|
+
} else if (event.key === 'ArrowDown') {
|
|
1237
|
+
event.preventDefault();
|
|
1238
|
+
commitLogHeight(Math.max(LOG_MIN_HEIGHT, Math.round(current - step)));
|
|
1239
|
+
} else if (event.key === 'Home') {
|
|
1240
|
+
event.preventDefault();
|
|
1241
|
+
commitLogHeight(LOG_MIN_HEIGHT);
|
|
1242
|
+
} else if (event.key === 'End') {
|
|
1243
|
+
event.preventDefault();
|
|
1244
|
+
commitLogHeight(logHeightLimit());
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
var resetLogHeight = function () {
|
|
1249
|
+
commitLogHeight(null);
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1084
1252
|
var kindLabel = function (kind) {
|
|
1085
1253
|
var key = 'kind.' + kind;
|
|
1086
1254
|
var text = t(key);
|
|
@@ -1099,7 +1267,7 @@
|
|
|
1099
1267
|
|
|
1100
1268
|
return h(
|
|
1101
1269
|
'div',
|
|
1102
|
-
{ className: 'dvb-root' },
|
|
1270
|
+
{ className: 'dvb-root', ref: rootRef },
|
|
1103
1271
|
h(
|
|
1104
1272
|
'div',
|
|
1105
1273
|
{ className: 'dvb-bar' },
|
|
@@ -1294,9 +1462,33 @@
|
|
|
1294
1462
|
state.headless ? h('span', null, t('hint.headless')) : null,
|
|
1295
1463
|
)
|
|
1296
1464
|
: null,
|
|
1465
|
+
logOpen
|
|
1466
|
+
? h('button', {
|
|
1467
|
+
type: 'button',
|
|
1468
|
+
ref: splitRef,
|
|
1469
|
+
className: 'dvb-split',
|
|
1470
|
+
role: 'separator',
|
|
1471
|
+
'aria-orientation': 'horizontal',
|
|
1472
|
+
'aria-label': t('timeline.resize'),
|
|
1473
|
+
'aria-valuemin': LOG_MIN_HEIGHT,
|
|
1474
|
+
'aria-valuemax': logHeightLimit(),
|
|
1475
|
+
'aria-valuenow': logHeight !== null ? logHeight : undefined,
|
|
1476
|
+
title: t('timeline.resize'),
|
|
1477
|
+
'data-active': String(resizing),
|
|
1478
|
+
onPointerDown: onSplitDown,
|
|
1479
|
+
onKeyDown: onSplitKeyDown,
|
|
1480
|
+
onDoubleClick: resetLogHeight,
|
|
1481
|
+
})
|
|
1482
|
+
: null,
|
|
1297
1483
|
h(
|
|
1298
1484
|
'div',
|
|
1299
|
-
{
|
|
1485
|
+
{
|
|
1486
|
+
className: 'dvb-log',
|
|
1487
|
+
ref: logRef,
|
|
1488
|
+
'data-open': String(logOpen),
|
|
1489
|
+
'data-sized': String(logHeight !== null),
|
|
1490
|
+
style: logHeight !== null ? { '--dvb-log-h': logHeight + 'px' } : undefined,
|
|
1491
|
+
},
|
|
1300
1492
|
h(
|
|
1301
1493
|
'button',
|
|
1302
1494
|
{
|
|
@@ -1317,22 +1509,22 @@
|
|
|
1317
1509
|
reachable ? null : h('span', { className: 'dvb-status' }, t('state.offline')),
|
|
1318
1510
|
),
|
|
1319
1511
|
logOpen
|
|
1320
|
-
?
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
{ className: 'dvb-log-
|
|
1325
|
-
actions.map(function (action) {
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1512
|
+
? h(
|
|
1513
|
+
'div',
|
|
1514
|
+
{ className: 'dvb-log-body', ref: logBodyRef },
|
|
1515
|
+
actions.length === 0
|
|
1516
|
+
? h('div', { className: 'dvb-log-empty' }, t('timeline.empty'))
|
|
1517
|
+
: actions.map(function (action) {
|
|
1518
|
+
var label = action.detail || action.target || kindLabel(action.kind);
|
|
1519
|
+
return h(
|
|
1520
|
+
'div',
|
|
1521
|
+
{ key: action.seq, className: 'dvb-row', title: label },
|
|
1522
|
+
h('span', { className: 'dvb-row-time' }, clock(action.ts)),
|
|
1523
|
+
h('span', { className: 'dvb-row-kind', 'data-kind': action.kind }, kindLabel(action.kind)),
|
|
1524
|
+
h('span', { className: 'dvb-row-text', 'data-error': String(action.ok === false) }, label),
|
|
1525
|
+
);
|
|
1526
|
+
}),
|
|
1527
|
+
)
|
|
1336
1528
|
: null,
|
|
1337
1529
|
),
|
|
1338
1530
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@restregular/dsh-visual-browser",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Visual browser for DeepSeek Harness: a right-sidebar tab under the built-in Browser that streams Chrome live as a binary JPEG frame stream (no screenshot polling, no per-frame request) while the Agent drives it through a token-light numbered view of interactive elements, never the raw DOM. Clicks, drags, wheel and typing are forwarded straight to the DevTools input domain, and every action stays in a collapsible, scrollable operation log.",
|
|
5
5
|
"icon": "./icon.svg",
|
|
6
6
|
"type": "module",
|
package/test/package.test.mjs
CHANGED
|
@@ -62,6 +62,8 @@ assert.ok(client.includes('createImageBitmap'), 'frames decode off the main thre
|
|
|
62
62
|
assert.ok(client.includes('/stream?r='), 'the panel reads the binary frame stream');
|
|
63
63
|
assert.ok(!client.includes('previewSeq'), 'no frame replay is left in the panel');
|
|
64
64
|
assert.ok(client.includes("'dvb-log-body'"), 'the operation log is a scroll container');
|
|
65
|
+
assert.ok(client.includes("role: 'separator'"), 'the log height has a drag handle');
|
|
66
|
+
assert.ok(client.includes('LOG_HEIGHT_KEY') && client.includes('localStorage'), 'the chosen split is remembered');
|
|
65
67
|
assert.ok(client.includes('--dsw-alias-interactive-bg-hover'), 'hover uses the interactive token, not a surface token');
|
|
66
68
|
assert.ok(!/(^|\n)\s*(const|let)\s+[A-Za-z_$][\w$]*\s*=/.test(client.replace(/^\s*\/\/.*$/gm, '')) || client.trimStart().startsWith('(function'), 'client file is wrapped in an IIFE');
|
|
67
69
|
|