visionclaw 0.1.207-beta.10 → 0.1.207-beta.11

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.
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/obs/mini-app/status.ts"],"names":[],"mappings":"AAAA,wBAAgB,gBAAgB,IAAI,MAAM,CA2oBzC"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/obs/mini-app/status.ts"],"names":[],"mappings":"AAAA,wBAAgB,gBAAgB,IAAI,MAAM,CAsyBzC"}
@@ -96,6 +96,60 @@ export function renderStatusHtml() {
96
96
  max-width: 78%;
97
97
  word-break: break-all;
98
98
  }
99
+ /* The copy icon eats ~19px of the value column, which is enough to wrap an
100
+ address that used to fit on one line. Buy that width back. A class, not
101
+ :has(), so this does not depend on selector support in older WebViews. */
102
+ .card-row .value.wide.copyable {
103
+ max-width: 84%;
104
+ }
105
+ /* Telegram's mobile WebView blocks long-press text selection, so a
106
+ plain-text value cannot be copied at all. A value that exists to be
107
+ copied (the email) renders as a button that writes to the clipboard.
108
+ It must look like the surrounding text, not like chrome. */
109
+ .card-row .value .copy-btn {
110
+ display: inline-flex;
111
+ /* Top, not baseline: when the address wraps, the icon should sit beside
112
+ the first line rather than drift down with the last one. */
113
+ align-items: flex-start;
114
+ gap: 6px;
115
+ background: none;
116
+ border: none;
117
+ padding: 0;
118
+ margin: 0;
119
+ font: inherit;
120
+ color: inherit;
121
+ text-align: right;
122
+ cursor: pointer;
123
+ /* Still allow native selection wherever the WebView permits it. */
124
+ -webkit-user-select: text;
125
+ user-select: text;
126
+ }
127
+ /* The address breaks inside its own column, so the icon never lands
128
+ mid-token and never strands a single character on a second line. */
129
+ .card-row .value .copy-text {
130
+ min-width: 0;
131
+ word-break: break-all;
132
+ }
133
+ .card-row .value .copy-icon {
134
+ font-size: 13px;
135
+ opacity: 0.65;
136
+ flex-shrink: 0;
137
+ }
138
+ .card-row .value .copy-btn.copied { color: #2e7d32; }
139
+ .card-row .value .copy-btn.copied .copy-icon { opacity: 1; }
140
+ /* The copy result is conveyed by an aria-hidden glyph, so the live region
141
+ below carries it for screen readers. Kept in the a11y tree, off-screen. */
142
+ .sr-only {
143
+ position: absolute;
144
+ width: 1px;
145
+ height: 1px;
146
+ margin: -1px;
147
+ padding: 0;
148
+ overflow: hidden;
149
+ clip: rect(0, 0, 0, 0);
150
+ white-space: nowrap;
151
+ border: 0;
152
+ }
99
153
  .badge {
100
154
  display: inline-flex;
101
155
  align-items: center;
@@ -346,12 +400,16 @@ export function renderStatusHtml() {
346
400
  .step-icon.action { background: #0d47a1; }
347
401
  .step-icon.result { background: #4a148c; }
348
402
  .task-elapsed { background: #e65100; color: #ffe0b2; }
403
+ .card-row .value .copy-btn.copied { color: #81c784; }
349
404
  .progress-shimmer { background: linear-gradient(90deg, #0d47a1 25%, #42a5f5 50%, #0d47a1 75%); background-size: 200% 100%; }
350
405
  }
351
406
  </style>
352
407
  </head>
353
408
  <body>
354
409
  <div id="app"><div class="loading">Loading status…</div></div>
410
+ <!-- Outside #app on purpose: render() replaces #app.innerHTML every 5s, which
411
+ would destroy the live region (and any pending announcement) mid-poll. -->
412
+ <div id="copy-status" class="sr-only" role="status" aria-live="polite"></div>
355
413
  <script>
356
414
  const tg = window.Telegram?.WebApp;
357
415
  if (tg) {
@@ -505,7 +563,7 @@ export function renderStatusHtml() {
505
563
  '<div class="card">' +
506
564
  '<div class="card-title">System</div>' +
507
565
  row('Status', '<span class="badge ' + statusClass + '">' + statusIcon + ' ' + statusLabel + '</span>') +
508
- row('Email', esc(d.agentEmail || '—'), 'wide') +
566
+ copyRow('Email', d.agentEmail, 'wide') +
509
567
  row('Uptime', esc(d.uptime || '—')) +
510
568
  row('Heartbeat', esc(d.heartbeat || '—')) +
511
569
  row('Model', esc(d.model || '—')) +
@@ -536,11 +594,33 @@ export function renderStatusHtml() {
536
594
  '">' + value + '</span></div>';
537
595
  }
538
596
 
597
+ // A row whose value is meant to be copied. Falls back to a plain row when
598
+ // there is nothing to copy, so the em dash never looks tappable.
599
+ function copyRow(label, value, valueClass) {
600
+ if (!value) return row(label, '—', valueClass);
601
+ return row(
602
+ label,
603
+ '<button type="button" class="copy-btn" data-copy="' + escAttr(value) +
604
+ '" data-copy-label="' + escAttr(label) +
605
+ '" aria-label="Copy ' + escAttr(label) + '">' +
606
+ '<span class="copy-text">' + esc(value) + '</span>' +
607
+ '<span class="copy-icon" aria-hidden="true">📋</span>' +
608
+ '</button>',
609
+ (valueClass ? valueClass + ' ' : '') + 'copyable',
610
+ );
611
+ }
612
+
539
613
  function esc(s) {
540
614
  if (!s) return '';
541
615
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
542
616
  }
543
617
 
618
+ // Attribute values additionally need quotes escaped, or a crafted value
619
+ // could close the attribute and inject markup.
620
+ function escAttr(s) {
621
+ return esc(s).replace(/"/g,'&quot;').replace(/'/g,'&#39;');
622
+ }
623
+
544
624
  function actionTimestamp(action) {
545
625
  return typeof action === 'object' && action ? Number(action.timestamp) || 0 : 0;
546
626
  }
@@ -644,6 +724,81 @@ export function renderStatusHtml() {
644
724
  return { icon: isAction ? '⚙️' : '📋', label: isResult ? humanName + ' — done' : humanName, detail: detail, type: isResult ? 'result' : 'action' };
645
725
  }
646
726
 
727
+ async function writeClipboard(text) {
728
+ try {
729
+ if (navigator.clipboard && navigator.clipboard.writeText) {
730
+ await navigator.clipboard.writeText(text);
731
+ return true;
732
+ }
733
+ } catch (e) {
734
+ // Fall through: the async API is unavailable on insecure origins and
735
+ // rejects without a user-gesture in some WebViews.
736
+ }
737
+ // Held outside the try: execCommand throwing is a normal failure mode in
738
+ // the very WebViews this fallback exists for, and the node must still be
739
+ // detached on that path or every tap leaks one.
740
+ let ta = null;
741
+ try {
742
+ ta = document.createElement('textarea');
743
+ ta.value = text;
744
+ ta.setAttribute('readonly', '');
745
+ ta.style.position = 'fixed';
746
+ ta.style.opacity = '0';
747
+ document.body.appendChild(ta);
748
+ ta.select();
749
+ ta.setSelectionRange(0, text.length);
750
+ return document.execCommand('copy');
751
+ } catch (e) {
752
+ return false;
753
+ } finally {
754
+ if (ta && ta.isConnected) ta.remove();
755
+ }
756
+ }
757
+
758
+ // Lives outside #app, so the 5s re-render cannot drop it. Writing the same
759
+ // string twice would not re-announce, so blank it first.
760
+ function announceCopy(message) {
761
+ const region = document.getElementById('copy-status');
762
+ if (!region) return;
763
+ region.textContent = '';
764
+ if (message) region.textContent = message;
765
+ }
766
+
767
+ // render() replaces #app.innerHTML every 5s, which would destroy a listener
768
+ // bound to the button itself — so delegate from the container instead.
769
+ let copyResetTimer = null;
770
+ document.getElementById('app').addEventListener('click', async (event) => {
771
+ const btn = event.target.closest ? event.target.closest('.copy-btn') : null;
772
+ if (!btn) return;
773
+ const text = btn.getAttribute('data-copy') || '';
774
+ if (!text) return;
775
+
776
+ const ok = await writeClipboard(text);
777
+ const icon = btn.querySelector('.copy-icon');
778
+ btn.classList.toggle('copied', ok);
779
+ if (icon) icon.textContent = ok ? '✅' : '⚠️';
780
+ // The glyph is aria-hidden, so the outcome only reaches a screen reader
781
+ // through the live region.
782
+ const what = btn.getAttribute('data-copy-label') || 'Value';
783
+ announceCopy(ok ? what + ' copied to clipboard' : 'Could not copy ' + what);
784
+ try {
785
+ tg?.HapticFeedback?.notificationOccurred(ok ? 'success' : 'error');
786
+ } catch (e) {
787
+ // HapticFeedback is absent outside the Telegram client.
788
+ }
789
+
790
+ // Revert the affordance so a later copy still reads as feedback. The
791
+ // next poll re-renders the row anyway; this covers the gap until then.
792
+ if (copyResetTimer) clearTimeout(copyResetTimer);
793
+ copyResetTimer = setTimeout(() => {
794
+ btn.classList.remove('copied');
795
+ if (icon) icon.textContent = '📋';
796
+ // Clear the announcement too, so the stale outcome is not re-read and
797
+ // the next copy of the same value announces again.
798
+ announceCopy('');
799
+ }, 1500);
800
+ });
801
+
647
802
  load();
648
803
  setInterval(load, 5000);
649
804
  </script>
@@ -1 +1 @@
1
- {"version":3,"file":"status.js","sourceRoot":"","sources":["../../../src/obs/mini-app/status.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,gBAAgB;IAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAyoBD,CAAC;AACT,CAAC"}
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../../src/obs/mini-app/status.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,gBAAgB;IAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAoyBD,CAAC;AACT,CAAC"}
@@ -15974,7 +15974,7 @@ ${DIM}${"\u2500".repeat(70)}${RESET}`);
15974
15974
 
15975
15975
  // dist/utils/version-check.js
15976
15976
  function isBundled() {
15977
- const v16 = "0.1.207-beta.10";
15977
+ const v16 = "0.1.207-beta.11";
15978
15978
  return typeof v16 === "string" && v16 !== "undefined";
15979
15979
  }
15980
15980
  function getPackageRoot() {
@@ -15991,7 +15991,7 @@ function getInstallationInfo() {
15991
15991
  };
15992
15992
  }
15993
15993
  function getCurrentVersion() {
15994
- const bundledVersion = "0.1.207-beta.10";
15994
+ const bundledVersion = "0.1.207-beta.11";
15995
15995
  if (bundledVersion && bundledVersion !== "undefined") {
15996
15996
  return bundledVersion;
15997
15997
  }
@@ -977763,7 +977763,7 @@ var init_playwriter_extension = __esm({
977763
977763
 
977764
977764
  // dist/utils/playwriter-relay.js
977765
977765
  function isBundled2() {
977766
- const v16 = "0.1.207-beta.10";
977766
+ const v16 = "0.1.207-beta.11";
977767
977767
  return typeof v16 === "string" && v16 !== "undefined";
977768
977768
  }
977769
977769
  function resolveRelayEntryPath() {
@@ -977889,7 +977889,7 @@ function selectAndPruneCanonicalRelay(matches, expectedEntry) {
977889
977889
  return canonical;
977890
977890
  }
977891
977891
  function pm2Start(entryPath, host, port) {
977892
- const versionStr = "0.1.207-beta.10";
977892
+ const versionStr = "0.1.207-beta.11";
977893
977893
  (0, import_node_child_process13.execFileSync)("pm2", ["start", entryPath, "--name", PM2_PROCESS_NAME, "--update-env"], {
977894
977894
  encoding: "utf-8",
977895
977895
  stdio: "pipe",
@@ -977915,7 +977915,7 @@ function pm2Delete() {
977915
977915
  }
977916
977916
  }
977917
977917
  function pm2Restart(host, port) {
977918
- const versionStr = "0.1.207-beta.10";
977918
+ const versionStr = "0.1.207-beta.11";
977919
977919
  (0, import_node_child_process13.execFileSync)("pm2", ["restart", PM2_PROCESS_NAME, "--update-env"], {
977920
977920
  encoding: "utf-8",
977921
977921
  stdio: "pipe",
@@ -978384,7 +978384,7 @@ async function reconcilePlaywriterRelay(host = "127.0.0.1", port = 19988) {
978384
978384
  pm2Restart(host, port);
978385
978385
  return;
978386
978386
  }
978387
- const currentVersion = "0.1.207-beta.10";
978387
+ const currentVersion = "0.1.207-beta.11";
978388
978388
  if (currentEnv.VISIONCLAW_VERSION && currentEnv.VISIONCLAW_VERSION !== currentVersion) {
978389
978389
  if (!await ensureRelayPortAvailable(host, port, current, "recreate after a version change"))
978390
978390
  return;
@@ -1422583,6 +1422583,60 @@ function renderStatusHtml() {
1422583
1422583
  max-width: 78%;
1422584
1422584
  word-break: break-all;
1422585
1422585
  }
1422586
+ /* The copy icon eats ~19px of the value column, which is enough to wrap an
1422587
+ address that used to fit on one line. Buy that width back. A class, not
1422588
+ :has(), so this does not depend on selector support in older WebViews. */
1422589
+ .card-row .value.wide.copyable {
1422590
+ max-width: 84%;
1422591
+ }
1422592
+ /* Telegram's mobile WebView blocks long-press text selection, so a
1422593
+ plain-text value cannot be copied at all. A value that exists to be
1422594
+ copied (the email) renders as a button that writes to the clipboard.
1422595
+ It must look like the surrounding text, not like chrome. */
1422596
+ .card-row .value .copy-btn {
1422597
+ display: inline-flex;
1422598
+ /* Top, not baseline: when the address wraps, the icon should sit beside
1422599
+ the first line rather than drift down with the last one. */
1422600
+ align-items: flex-start;
1422601
+ gap: 6px;
1422602
+ background: none;
1422603
+ border: none;
1422604
+ padding: 0;
1422605
+ margin: 0;
1422606
+ font: inherit;
1422607
+ color: inherit;
1422608
+ text-align: right;
1422609
+ cursor: pointer;
1422610
+ /* Still allow native selection wherever the WebView permits it. */
1422611
+ -webkit-user-select: text;
1422612
+ user-select: text;
1422613
+ }
1422614
+ /* The address breaks inside its own column, so the icon never lands
1422615
+ mid-token and never strands a single character on a second line. */
1422616
+ .card-row .value .copy-text {
1422617
+ min-width: 0;
1422618
+ word-break: break-all;
1422619
+ }
1422620
+ .card-row .value .copy-icon {
1422621
+ font-size: 13px;
1422622
+ opacity: 0.65;
1422623
+ flex-shrink: 0;
1422624
+ }
1422625
+ .card-row .value .copy-btn.copied { color: #2e7d32; }
1422626
+ .card-row .value .copy-btn.copied .copy-icon { opacity: 1; }
1422627
+ /* The copy result is conveyed by an aria-hidden glyph, so the live region
1422628
+ below carries it for screen readers. Kept in the a11y tree, off-screen. */
1422629
+ .sr-only {
1422630
+ position: absolute;
1422631
+ width: 1px;
1422632
+ height: 1px;
1422633
+ margin: -1px;
1422634
+ padding: 0;
1422635
+ overflow: hidden;
1422636
+ clip: rect(0, 0, 0, 0);
1422637
+ white-space: nowrap;
1422638
+ border: 0;
1422639
+ }
1422586
1422640
  .badge {
1422587
1422641
  display: inline-flex;
1422588
1422642
  align-items: center;
@@ -1422833,12 +1422887,16 @@ function renderStatusHtml() {
1422833
1422887
  .step-icon.action { background: #0d47a1; }
1422834
1422888
  .step-icon.result { background: #4a148c; }
1422835
1422889
  .task-elapsed { background: #e65100; color: #ffe0b2; }
1422890
+ .card-row .value .copy-btn.copied { color: #81c784; }
1422836
1422891
  .progress-shimmer { background: linear-gradient(90deg, #0d47a1 25%, #42a5f5 50%, #0d47a1 75%); background-size: 200% 100%; }
1422837
1422892
  }
1422838
1422893
  </style>
1422839
1422894
  </head>
1422840
1422895
  <body>
1422841
1422896
  <div id="app"><div class="loading">Loading status\u2026</div></div>
1422897
+ <!-- Outside #app on purpose: render() replaces #app.innerHTML every 5s, which
1422898
+ would destroy the live region (and any pending announcement) mid-poll. -->
1422899
+ <div id="copy-status" class="sr-only" role="status" aria-live="polite"></div>
1422842
1422900
  <script>
1422843
1422901
  const tg = window.Telegram?.WebApp;
1422844
1422902
  if (tg) {
@@ -1422992,7 +1423050,7 @@ function renderStatusHtml() {
1422992
1423050
  '<div class="card">' +
1422993
1423051
  '<div class="card-title">System</div>' +
1422994
1423052
  row('Status', '<span class="badge ' + statusClass + '">' + statusIcon + ' ' + statusLabel + '</span>') +
1422995
- row('Email', esc(d.agentEmail || '\u2014'), 'wide') +
1423053
+ copyRow('Email', d.agentEmail, 'wide') +
1422996
1423054
  row('Uptime', esc(d.uptime || '\u2014')) +
1422997
1423055
  row('Heartbeat', esc(d.heartbeat || '\u2014')) +
1422998
1423056
  row('Model', esc(d.model || '\u2014')) +
@@ -1423023,11 +1423081,33 @@ function renderStatusHtml() {
1423023
1423081
  '">' + value + '</span></div>';
1423024
1423082
  }
1423025
1423083
 
1423084
+ // A row whose value is meant to be copied. Falls back to a plain row when
1423085
+ // there is nothing to copy, so the em dash never looks tappable.
1423086
+ function copyRow(label, value, valueClass) {
1423087
+ if (!value) return row(label, '\u2014', valueClass);
1423088
+ return row(
1423089
+ label,
1423090
+ '<button type="button" class="copy-btn" data-copy="' + escAttr(value) +
1423091
+ '" data-copy-label="' + escAttr(label) +
1423092
+ '" aria-label="Copy ' + escAttr(label) + '">' +
1423093
+ '<span class="copy-text">' + esc(value) + '</span>' +
1423094
+ '<span class="copy-icon" aria-hidden="true">\u{1F4CB}</span>' +
1423095
+ '</button>',
1423096
+ (valueClass ? valueClass + ' ' : '') + 'copyable',
1423097
+ );
1423098
+ }
1423099
+
1423026
1423100
  function esc(s) {
1423027
1423101
  if (!s) return '';
1423028
1423102
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
1423029
1423103
  }
1423030
1423104
 
1423105
+ // Attribute values additionally need quotes escaped, or a crafted value
1423106
+ // could close the attribute and inject markup.
1423107
+ function escAttr(s) {
1423108
+ return esc(s).replace(/"/g,'&quot;').replace(/'/g,'&#39;');
1423109
+ }
1423110
+
1423031
1423111
  function actionTimestamp(action) {
1423032
1423112
  return typeof action === 'object' && action ? Number(action.timestamp) || 0 : 0;
1423033
1423113
  }
@@ -1423131,6 +1423211,81 @@ function renderStatusHtml() {
1423131
1423211
  return { icon: isAction ? '\u2699\uFE0F' : '\u{1F4CB}', label: isResult ? humanName + ' \u2014 done' : humanName, detail: detail, type: isResult ? 'result' : 'action' };
1423132
1423212
  }
1423133
1423213
 
1423214
+ async function writeClipboard(text) {
1423215
+ try {
1423216
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1423217
+ await navigator.clipboard.writeText(text);
1423218
+ return true;
1423219
+ }
1423220
+ } catch (e) {
1423221
+ // Fall through: the async API is unavailable on insecure origins and
1423222
+ // rejects without a user-gesture in some WebViews.
1423223
+ }
1423224
+ // Held outside the try: execCommand throwing is a normal failure mode in
1423225
+ // the very WebViews this fallback exists for, and the node must still be
1423226
+ // detached on that path or every tap leaks one.
1423227
+ let ta = null;
1423228
+ try {
1423229
+ ta = document.createElement('textarea');
1423230
+ ta.value = text;
1423231
+ ta.setAttribute('readonly', '');
1423232
+ ta.style.position = 'fixed';
1423233
+ ta.style.opacity = '0';
1423234
+ document.body.appendChild(ta);
1423235
+ ta.select();
1423236
+ ta.setSelectionRange(0, text.length);
1423237
+ return document.execCommand('copy');
1423238
+ } catch (e) {
1423239
+ return false;
1423240
+ } finally {
1423241
+ if (ta && ta.isConnected) ta.remove();
1423242
+ }
1423243
+ }
1423244
+
1423245
+ // Lives outside #app, so the 5s re-render cannot drop it. Writing the same
1423246
+ // string twice would not re-announce, so blank it first.
1423247
+ function announceCopy(message) {
1423248
+ const region = document.getElementById('copy-status');
1423249
+ if (!region) return;
1423250
+ region.textContent = '';
1423251
+ if (message) region.textContent = message;
1423252
+ }
1423253
+
1423254
+ // render() replaces #app.innerHTML every 5s, which would destroy a listener
1423255
+ // bound to the button itself \u2014 so delegate from the container instead.
1423256
+ let copyResetTimer = null;
1423257
+ document.getElementById('app').addEventListener('click', async (event) => {
1423258
+ const btn = event.target.closest ? event.target.closest('.copy-btn') : null;
1423259
+ if (!btn) return;
1423260
+ const text = btn.getAttribute('data-copy') || '';
1423261
+ if (!text) return;
1423262
+
1423263
+ const ok = await writeClipboard(text);
1423264
+ const icon = btn.querySelector('.copy-icon');
1423265
+ btn.classList.toggle('copied', ok);
1423266
+ if (icon) icon.textContent = ok ? '\u2705' : '\u26A0\uFE0F';
1423267
+ // The glyph is aria-hidden, so the outcome only reaches a screen reader
1423268
+ // through the live region.
1423269
+ const what = btn.getAttribute('data-copy-label') || 'Value';
1423270
+ announceCopy(ok ? what + ' copied to clipboard' : 'Could not copy ' + what);
1423271
+ try {
1423272
+ tg?.HapticFeedback?.notificationOccurred(ok ? 'success' : 'error');
1423273
+ } catch (e) {
1423274
+ // HapticFeedback is absent outside the Telegram client.
1423275
+ }
1423276
+
1423277
+ // Revert the affordance so a later copy still reads as feedback. The
1423278
+ // next poll re-renders the row anyway; this covers the gap until then.
1423279
+ if (copyResetTimer) clearTimeout(copyResetTimer);
1423280
+ copyResetTimer = setTimeout(() => {
1423281
+ btn.classList.remove('copied');
1423282
+ if (icon) icon.textContent = '\u{1F4CB}';
1423283
+ // Clear the announcement too, so the stale outcome is not re-read and
1423284
+ // the next copy of the same value announces again.
1423285
+ announceCopy('');
1423286
+ }, 1500);
1423287
+ });
1423288
+
1423134
1423289
  load();
1423135
1423290
  setInterval(load, 5000);
1423136
1423291
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "visionclaw",
3
- "version": "0.1.207-beta.10",
3
+ "version": "0.1.207-beta.11",
4
4
  "description": "A personal assistant agent that runs on your desktop, receives commands from messaging channels, and executes tasks autonomously.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",