@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 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**. Records only: there is nothing to replay.
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
- { className: 'dvb-log', 'data-open': String(logOpen) },
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
- ? actions.length === 0
1321
- ? h('div', { className: 'dvb-log-empty' }, t('timeline.empty'))
1322
- : h(
1323
- 'div',
1324
- { className: 'dvb-log-body' },
1325
- actions.map(function (action) {
1326
- var label = action.detail || action.target || kindLabel(action.kind);
1327
- return h(
1328
- 'div',
1329
- { key: action.seq, className: 'dvb-row', title: label },
1330
- h('span', { className: 'dvb-row-time' }, clock(action.ts)),
1331
- h('span', { className: 'dvb-row-kind', 'data-kind': action.kind }, kindLabel(action.kind)),
1332
- h('span', { className: 'dvb-row-text', 'data-error': String(action.ok === false) }, label),
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.0",
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",
@@ -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