@wenathlan/devthink 2.0.12 → 2.0.14
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 +20 -0
- package/accounts.ts +2087 -0
- package/agent.ts +2331 -0
- package/antigravity.ts +2139 -0
- package/apifreeze.ts +4146 -0
- package/auth.ts +642 -0
- package/background.ts +16837 -0
- package/bridge.ts +2080 -0
- package/build.ts +54 -0
- package/bun.ts +26 -0
- package/capture.ts +1614 -0
- package/checksecrets.ts +72 -0
- package/cjs.ts +9 -0
- package/cli.ts +3500 -0
- package/commands.ts +1101 -0
- package/companion.ts +450 -0
- package/compatibility.ts +37 -0
- package/config.ts +3137 -0
- package/constants.ts +943 -0
- package/core.ts +1473 -0
- package/crossbrowser.ts +1359 -0
- package/dashdone.ts +605 -0
- package/data.ts +943 -0
- package/database.ts +133 -0
- package/debug.ts +850 -0
- package/deno.ts +27 -0
- package/devthink.ts +707 -0
- package/dist/accounts.d.ts +409 -0
- package/dist/agent.d.ts +837 -0
- package/dist/antigravity.d.ts +380 -0
- package/dist/apifreeze.d.ts +113 -0
- package/dist/auth.d.ts +224 -0
- package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
- package/dist/capture.d.ts +595 -0
- package/dist/cjs.d.ts +6 -0
- package/{cli.js → dist/cli.js} +2 -2
- package/dist/commands.d.ts +310 -0
- package/dist/compatibility.d.ts +17 -0
- package/dist/config.d.ts +1853 -0
- package/dist/constants.d.ts +637 -0
- package/dist/core.d.ts +361 -0
- package/dist/data.d.ts +330 -0
- package/dist/database.d.ts +24 -0
- package/dist/debug.d.ts +270 -0
- package/dist/engine.d.ts +74 -0
- package/dist/environments.d.ts +244 -0
- package/dist/evidence.d.ts +277 -0
- package/dist/export.d.ts +311 -0
- package/dist/fingerprint.d.ts +594 -0
- package/dist/flow.d.ts +300 -0
- package/dist/gates.d.ts +103 -0
- package/dist/identity.d.ts +51 -0
- package/dist/ids.d.ts +7 -0
- package/dist/llm.d.ts +652 -0
- package/dist/models.d.ts +1807 -0
- package/dist/net.d.ts +325 -0
- package/dist/neutral.d.ts +41 -0
- package/dist/oauth.d.ts +703 -0
- package/dist/page.d.ts +1166 -0
- package/dist/perf.d.ts +481 -0
- package/dist/plan.d.ts +134 -0
- package/dist/project.d.ts +395 -0
- package/dist/providers.d.ts +37 -0
- package/dist/quota.d.ts +453 -0
- package/dist/recovery.d.ts +621 -0
- package/dist/request.d.ts +1492 -0
- package/dist/run.d.ts +779 -0
- package/dist/runtime.d.ts +212 -0
- package/dist/search.d.ts +375 -0
- package/dist/security.d.ts +534 -0
- package/dist/serve.d.ts +136 -0
- package/dist/session.d.ts +296 -0
- package/dist/storage.d.ts +53 -0
- package/dist/streaming.d.ts +1119 -0
- package/dist/swarm.d.ts +328 -0
- package/dist/tools.d.ts +170 -0
- package/dist/types.d.ts +8357 -0
- package/dist/utils.d.ts +82 -0
- package/dist/version.d.ts +7 -0
- package/dist/versionregistry.d.ts +406 -0
- package/dist/views.d.ts +579 -0
- package/dist/workbenchsession.d.ts +69 -0
- package/dist/workflow.d.ts +649 -0
- package/docs/readme.md +92 -0
- package/docs/schemas/antigravity.json +192 -0
- package/engine.ts +1524 -0
- package/environments.ts +1005 -0
- package/evidence.ts +643 -0
- package/export.ts +889 -0
- package/fingerprint.ts +1458 -0
- package/flow.ts +1190 -0
- package/gates.ts +307 -0
- package/gateway.ts +34 -0
- package/hardening.ts +191 -0
- package/headless.ts +369 -0
- package/http.ts +2110 -0
- package/identity.ts +246 -0
- package/ids.ts +26 -0
- package/index.ts +369 -0
- package/llm.ts +2682 -0
- package/mcp.ts +1835 -0
- package/{memory.js → memory.ts} +3834 -2310
- package/models.ts +3805 -0
- package/modes.ts +43 -0
- package/net.ts +1214 -0
- package/neutral.ts +362 -0
- package/node.ts +26 -0
- package/oauth.ts +2216 -0
- package/offscreen.ts +170 -0
- package/pack.ts +956 -0
- package/package.json +175 -82
- package/page.ts +7824 -0
- package/pagebridge.ts +1116 -0
- package/perf.ts +957 -0
- package/plan.ts +672 -0
- package/plugin.ts +76 -0
- package/{policy.js → policy.ts} +5073 -4679
- package/prisma.config.ts +27 -0
- package/progress.ts +1308 -0
- package/project.ts +1291 -0
- package/protocol.ts +4187 -0
- package/providers.ts +246 -0
- package/quota.ts +1887 -0
- package/recovery.ts +2433 -0
- package/request.ts +4482 -0
- package/run.ts +2135 -0
- package/runtime.ts +526 -0
- package/search.ts +1760 -0
- package/security.ts +1673 -0
- package/serve.ts +419 -0
- package/server.ts +5538 -0
- package/session.ts +1162 -0
- package/storage.ts +182 -0
- package/streaming.ts +4101 -0
- package/swarm.ts +884 -0
- package/sync.ts +56 -0
- package/tests/agentcert.test.ts +128 -0
- package/tests/agentfleet.test.ts +214 -0
- package/tests/agentreview.test.ts +241 -0
- package/tests/agentstream.test.ts +335 -0
- package/tests/agentwork.test.ts +356 -0
- package/tests/apifreeze.test.ts +290 -0
- package/tests/apimap.test.ts +112 -0
- package/tests/approvalgate.test.ts +175 -0
- package/tests/artifactmanifest.test.ts +204 -0
- package/tests/attentionfeed.test.ts +163 -0
- package/tests/backgroundruns.test.ts +100 -0
- package/tests/batchscheduling.test.ts +131 -0
- package/tests/blackboard.test.ts +238 -0
- package/tests/bridge.test.ts +1088 -0
- package/tests/browsercoverage.test.ts +134 -0
- package/tests/browserpolyfills.test.ts +124 -0
- package/tests/build.mjs +1260 -0
- package/tests/clientauth.test.ts +223 -0
- package/tests/clitools.test.ts +578 -0
- package/tests/confirmgates.test.ts +190 -0
- package/tests/containerpack.test.ts +143 -0
- package/tests/controlflow.test.ts +2120 -0
- package/tests/coordination.test.ts +345 -0
- package/tests/copyscreen.test.ts +139 -0
- package/tests/core.test.ts +417 -0
- package/tests/costcert.test.ts +95 -0
- package/tests/crossbrowser.test.ts +115 -0
- package/tests/cspaudit.test.ts +79 -0
- package/tests/dashdone.test.ts +615 -0
- package/tests/datagrid.test.ts +137 -0
- package/tests/doccheck.test.ts +107 -0
- package/tests/dombatch.test.ts +109 -0
- package/tests/emulation.test.ts +282 -0
- package/tests/environments.test.ts +259 -0
- package/tests/evidenceviews.test.ts +88 -0
- package/tests/exporttools.test.ts +185 -0
- package/tests/firefoxprep.test.ts +134 -0
- package/tests/flowlibrary.test.ts +242 -0
- package/tests/flowrun.test.ts +303 -0
- package/tests/forensics.test.ts +439 -0
- package/tests/gateway.test.ts +939 -0
- package/tests/headlesslib.test.ts +171 -0
- package/tests/httpstream.test.ts +277 -0
- package/tests/immutablelog.test.ts +157 -0
- package/tests/inboundguard.test.ts +177 -0
- package/tests/lazyload.test.ts +72 -0
- package/tests/librarymodes.test.ts +623 -0
- package/tests/llm.test.ts +696 -0
- package/tests/manifest.test.ts +226 -0
- package/tests/maskinputs.test.ts +150 -0
- package/tests/matrixverify.test.ts +111 -0
- package/tests/mavenpack.test.ts +75 -0
- package/tests/mcpserver.test.ts +704 -0
- package/tests/memory.test.ts +4155 -0
- package/tests/memorycare.test.ts +230 -0
- package/tests/migrateplan.test.ts +349 -0
- package/tests/minimization.test.ts +414 -0
- package/tests/modelroute.test.ts +112 -0
- package/tests/multiagent.test.ts +440 -0
- package/tests/nativebridge.test.ts +807 -0
- package/tests/navigation.test.ts +284 -0
- package/tests/nugetpack.test.ts +83 -0
- package/tests/orchestration.test.ts +449 -0
- package/tests/originpolicy.test.ts +405 -0
- package/tests/outputcompare.test.ts +97 -0
- package/tests/package.test.ts +91 -0
- package/tests/pagebridge.test.ts +5909 -0
- package/tests/pentest.test.ts +75 -0
- package/tests/perfrecords.test.ts +257 -0
- package/tests/permdiff.test.ts +85 -0
- package/tests/phishguard.test.ts +128 -0
- package/tests/pickerviews.test.ts +155 -0
- package/tests/pipelines.test.ts +381 -0
- package/tests/planlint.test.ts +240 -0
- package/tests/planreview.test.ts +383 -0
- package/tests/platformtargets.test.ts +56 -0
- package/tests/policy.test.ts +8284 -0
- package/tests/portability.test.ts +96 -0
- package/tests/progress.test.ts +1036 -0
- package/tests/promptlibrary.test.ts +116 -0
- package/tests/protocol.test.ts +4793 -0
- package/tests/publishingpipeline.test.ts +314 -0
- package/tests/quickactions.test.ts +164 -0
- package/tests/readiness.test.ts +78 -0
- package/tests/recipes.test.ts +163 -0
- package/tests/redactshots.test.ts +216 -0
- package/tests/registernvidiakeys.ts +51 -0
- package/tests/relayserve.test.ts +304 -0
- package/tests/releasenotes.test.ts +276 -0
- package/tests/resilience.test.ts +467 -0
- package/tests/resourceaware.test.ts +187 -0
- package/tests/runbudget.test.ts +125 -0
- package/tests/runhistory.test.ts +299 -0
- package/tests/runreplay.test.ts +113 -0
- package/tests/runresume.test.ts +184 -0
- package/tests/runstate.test.ts +288 -0
- package/tests/runtimeadapters.test.ts +251 -0
- package/tests/safariskeleton.test.ts +115 -0
- package/tests/sandboxframe.test.ts +196 -0
- package/tests/sbom.test.ts +105 -0
- package/tests/secretvault.test.ts +188 -0
- package/tests/selcache.test.ts +111 -0
- package/tests/sessioninterface.test.ts +791 -0
- package/tests/sessions.test.ts +591 -0
- package/tests/siteprefs.test.ts +228 -0
- package/tests/snapshotdelta.test.ts +165 -0
- package/tests/statusviews.test.ts +237 -0
- package/tests/stepmeter.test.ts +200 -0
- package/tests/streamparse.test.ts +163 -0
- package/tests/stylepruning.test.ts +90 -0
- package/tests/surfaces.test.ts +282 -0
- package/tests/sweep.test.ts +78 -0
- package/tests/syncbridge.test.ts +164 -0
- package/tests/taskqueue.test.ts +206 -0
- package/tests/toolcalls.test.ts +336 -0
- package/tests/toolcatalog.test.ts +201 -0
- package/tests/torture-agent.test.ts +706 -0
- package/tests/torture-auth.test.ts +461 -0
- package/tests/torture-commands.test.ts +895 -0
- package/tests/torture-data.test.ts +923 -0
- package/tests/torture-environments.test.ts +285 -0
- package/tests/torture-export.test.ts +263 -0
- package/tests/torture-http.test.ts +1379 -0
- package/tests/torture-llm.test.ts +808 -0
- package/tests/torture-memory.test.ts +1084 -0
- package/tests/torture-plan.test.ts +829 -0
- package/tests/torture-run.test.ts +626 -0
- package/tests/torture-session.test.ts +928 -0
- package/tests/torture-swarm.test.ts +1245 -0
- package/tests/torture-tools.test.ts +964 -0
- package/tests/torture-workflow.test.ts +2354 -0
- package/tests/tourviews.test.ts +52 -0
- package/tests/transparency.test.ts +147 -0
- package/tests/trigger.test.ts +941 -0
- package/tests/v1sunset.test.ts +428 -0
- package/tests/virtlist.test.ts +94 -0
- package/tests/vision.test.ts +540 -0
- package/tests/vsixpack.test.ts +145 -0
- package/tests/webapi.test.ts +523 -0
- package/tests/workflow.test.ts +1785 -0
- package/tests/workfloweditor.test.ts +759 -0
- package/tests/xpipack.test.ts +128 -0
- package/tools.ts +734 -0
- package/types.ts +9908 -0
- package/umd.ts +9 -0
- package/utils.ts +249 -0
- package/version.ts +8 -0
- package/versionregistry.ts +1310 -0
- package/views.ts +2153 -0
- package/vitest.config.ts +12 -0
- package/web/App.tsx +78 -0
- package/web/README.md +47 -0
- package/web/capacitor.config.ts +46 -0
- package/web/const.ts +19 -0
- package/web/control.shell.tsx +66 -0
- package/web/dashboardpage.ts +554 -0
- package/web/db.ts +493 -0
- package/web/design.html +541 -0
- package/web/gatewayview/Gateway.tsx +345 -0
- package/web/gatewayview/config.ts +1042 -0
- package/web/gatewayview/definition.ts +174 -0
- package/web/icons.ts +15 -0
- package/{manifest.json → web/manifest.json} +1 -1
- package/web/optionspage.ts +691 -0
- package/web/popup.ts +1651 -0
- package/web/schema.prisma +176 -0
- package/web/sidepanel.ts +7941 -0
- package/web/sitemanifest.json +13 -0
- package/web/terminal-ui.tsx +622 -0
- package/web/transparencypage.ts +541 -0
- package/web/vite.config.ts +39 -0
- package/web/workspace.ts +23 -0
- package/workbenchmemory.ts +104 -0
- package/workbenchsession.ts +304 -0
- package/workflow.ts +4495 -0
- package/bridge.d.ts.map +0 -1
- package/bridge.js +0 -1877
- package/bun.d.ts.map +0 -1
- package/bun.js +0 -52329
- package/caps/background.json +0 -712
- package/caps/cli.json +0 -360
- package/caps/library.json +0 -3611
- package/caps/mcp.json +0 -126
- package/caps/pagebridge.json +0 -377
- package/caps/popup.json +0 -51
- package/caps/sidepanel.json +0 -522
- package/checksums.txt +0 -142
- package/cli.d.ts.map +0 -1
- package/companion.d.ts.map +0 -1
- package/companion.js +0 -389
- package/crossbrowser.d.ts.map +0 -1
- package/crossbrowser.js +0 -1191
- package/dashdone.d.ts.map +0 -1
- package/dashdone.js +0 -368
- package/deno.d.ts.map +0 -1
- package/deno.js +0 -52325
- package/devthink.umd.js +0 -52346
- package/fixtures/bun.ts +0 -7
- package/fixtures/cjs.js +0 -6
- package/fixtures/deno.ts +0 -6
- package/fixtures/example-org-pagestate.json +0 -22
- package/fixtures/importers/automa-workflow.json +0 -18
- package/fixtures/importers/selenium-side.json +0 -20
- package/fixtures/importers/tabular-plan.csv +0 -7
- package/fixtures/importers/uivision-macro.json +0 -10
- package/fixtures/importers/v1-plan.json +0 -13
- package/fixtures/mcp-client.mjs +0 -14
- package/fixtures/pages/checkout.html +0 -192
- package/fixtures/pages/dashboard.html +0 -110
- package/fixtures/pages/feed.html +0 -113
- package/fixtures/pages/product-grid.html +0 -150
- package/fixtures/plans/form-inventory-plan.json +0 -11
- package/fixtures/plans/release-notes-plan.json +0 -11
- package/fixtures/recipes/agentscompete.json +0 -11
- package/fixtures/recipes/agentsforms.json +0 -11
- package/fixtures/recipes/agentsmonitor.json +0 -11
- package/fixtures/recipes/agentsreview.json +0 -29
- package/fixtures/recipes/agentswarm.json +0 -11
- package/fixtures/recipes/fillcalendar.json +0 -12
- package/fixtures/recipes/fillcheckout.json +0 -13
- package/fixtures/recipes/filldependentdropdowns.json +0 -12
- package/fixtures/recipes/fillfromcsv.json +0 -13
- package/fixtures/recipes/fillloginform.json +0 -12
- package/fixtures/recipes/fillmultistepwizard.json +0 -13
- package/fixtures/recipes/fillregistration.json +0 -12
- package/fixtures/recipes/fillsearchfilters.json +0 -12
- package/fixtures/recipes/filltypeahead.json +0 -11
- package/fixtures/recipes/monitoralerts.json +0 -12
- package/fixtures/recipes/monitoravailability.json +0 -11
- package/fixtures/recipes/monitornetworkquiet.json +0 -12
- package/fixtures/recipes/monitorpagechanges.json +0 -11
- package/fixtures/recipes/monitorpricechange.json +0 -11
- package/fixtures/recipes/monitorschedule.json +0 -12
- package/fixtures/recipes/scrapedynamicprices.json +0 -13
- package/fixtures/recipes/scrapeimagegallery.json +0 -12
- package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
- package/fixtures/recipes/scrapenestedlists.json +0 -12
- package/fixtures/recipes/scrapeproductgrid.json +0 -12
- package/fixtures/recipes/scrapereviews.json +0 -12
- package/fixtures/recipes/scrapesearchresults.json +0 -12
- package/fixtures/recipes/scrapetablepagination.json +0 -12
- package/fixtures/recipes/testaccessibility.json +0 -34
- package/fixtures/recipes/testconsoleerrors.json +0 -12
- package/fixtures/recipes/testdeeplinks.json +0 -12
- package/fixtures/recipes/testformvalidation.json +0 -13
- package/fixtures/recipes/testlayoutshift.json +0 -12
- package/fixtures/recipes/testlinkcheck.json +0 -12
- package/fixtures/recipes/testloadtiming.json +0 -12
- package/fixtures/recipes/testvisualdiff.json +0 -12
- package/fixtures/workflows/changelog-digest-workflow.json +0 -13
- package/fixtures/workflows/form-survey-workflow.json +0 -13
- package/gallery.json +0 -706
- package/hardening.d.ts.map +0 -1
- package/hardening.js +0 -188
- package/headless.d.ts.map +0 -1
- package/headless.js +0 -11522
- package/http.d.ts.map +0 -1
- package/http.js +0 -2165
- package/index.cjs +0 -54682
- package/index.d.ts.map +0 -1
- package/index.js +0 -52310
- package/index.neutral.js +0 -46343
- package/mcp.d.ts.map +0 -1
- package/mcp.js +0 -20515
- package/memory.d.ts.map +0 -1
- package/nativehost.template.json +0 -10
- package/node.cjs +0 -54686
- package/node.d.ts.map +0 -1
- package/pack.d.ts.map +0 -1
- package/pack.js +0 -805
- package/policy.d.ts.map +0 -1
- package/progress.d.ts.map +0 -1
- package/progress.js +0 -645
- package/protocol.d.ts.map +0 -1
- package/protocol.js +0 -8334
- package/schemas/audit.schema.json +0 -226
- package/schemas/capability.schema.json +0 -113
- package/schemas/envelope.schema.json +0 -162
- package/schemas/memory.schema.json +0 -51
- package/schemas/observation.schema.json +0 -113
- package/schemas/plan.schema.json +0 -208
- package/schemas/progress.schema.json +0 -126
- package/schemas/proposal.schema.json +0 -68
- package/schemas/session.schema.json +0 -82
- package/schemas/tool.schema.json +0 -94
- package/server.d.ts.map +0 -1
- package/server.js +0 -28877
- package/umd-example.html +0 -18
- package/umd.d.ts.map +0 -1
- /package/{bun.d.ts → dist/bun.d.ts} +0 -0
- /package/{cli.d.ts → dist/cli.d.ts} +0 -0
- /package/{companion.d.ts → dist/companion.d.ts} +0 -0
- /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
- /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
- /package/{deno.d.ts → dist/deno.d.ts} +0 -0
- /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
- /package/{headless.d.ts → dist/headless.d.ts} +0 -0
- /package/{http.d.ts → dist/http.d.ts} +0 -0
- /package/{index.d.ts → dist/index.d.ts} +0 -0
- /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
- /package/{memory.d.ts → dist/memory.d.ts} +0 -0
- /package/{node.d.ts → dist/node.d.ts} +0 -0
- /package/{pack.d.ts → dist/pack.d.ts} +0 -0
- /package/{policy.d.ts → dist/policy.d.ts} +0 -0
- /package/{progress.d.ts → dist/progress.d.ts} +0 -0
- /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
- /package/{server.d.ts → dist/server.d.ts} +0 -0
- /package/{umd.d.ts → dist/umd.d.ts} +0 -0
package/web/popup.ts
ADDED
|
@@ -0,0 +1,1651 @@
|
|
|
1
|
+
import { hostpattern, normalizeendpoint } from "../policy.js";
|
|
2
|
+
import { bridgestatuslabel } from "../bridge.js";
|
|
3
|
+
import type {
|
|
4
|
+
capabilityreport,
|
|
5
|
+
captchahandoff,
|
|
6
|
+
clipboardconsentrecord,
|
|
7
|
+
consoleconsentrecord,
|
|
8
|
+
dataset,
|
|
9
|
+
dialogdecision,
|
|
10
|
+
downloadrecord,
|
|
11
|
+
keyholdstate,
|
|
12
|
+
navqueues,
|
|
13
|
+
navstate,
|
|
14
|
+
quarantineentry,
|
|
15
|
+
streamstate,
|
|
16
|
+
tabbadge,
|
|
17
|
+
waitprofilerecord,
|
|
18
|
+
} from "../types.js";
|
|
19
|
+
import type { windowshape } from "../commands.js";
|
|
20
|
+
|
|
21
|
+
type pagesignallike = { language?: string; template?: string; scrolllocked?: boolean; banner?: string };
|
|
22
|
+
|
|
23
|
+
const endpointinput = document.querySelector<HTMLInputElement>("#endpoint");
|
|
24
|
+
const statusnode = document.querySelector<HTMLElement>("#status");
|
|
25
|
+
const capabilitiesnode = document.querySelector<HTMLElement>("#capabilities");
|
|
26
|
+
const livenode = document.querySelector<HTMLElement>("#livelogic");
|
|
27
|
+
const navstatenode = document.querySelector<HTMLElement>("#navstate");
|
|
28
|
+
const windowlayoutnode = document.querySelector<HTMLElement>("#windowlayout");
|
|
29
|
+
const captchastatenode = document.querySelector<HTMLElement>("#captchastate");
|
|
30
|
+
const datasetstatenode = document.querySelector<HTMLElement>("#datasetstate");
|
|
31
|
+
const downloadstatenode = document.querySelector<HTMLElement>("#downloadstate");
|
|
32
|
+
const clipboardstatenode = document.querySelector<HTMLElement>("#clipboardstate");
|
|
33
|
+
const capturestatenode = document.querySelector<HTMLElement>("#capturestate");
|
|
34
|
+
const mediastatenode = document.querySelector<HTMLElement>("#mediastate");
|
|
35
|
+
const callstatenode = document.querySelector<HTMLElement>("#callstate");
|
|
36
|
+
const rulestatenode = document.querySelector<HTMLElement>("#rulestate");
|
|
37
|
+
const netstatenode = document.querySelector<HTMLElement>("#netstate");
|
|
38
|
+
const debugstatenode = document.querySelector<HTMLElement>("#debugstate");
|
|
39
|
+
const emulationstatenode = document.querySelector<HTMLElement>("#emulationstate");
|
|
40
|
+
const sessionstatenode = document.querySelector<HTMLElement>("#sessionstate");
|
|
41
|
+
const workflowstatenode = document.querySelector<HTMLElement>("#workflowstate");
|
|
42
|
+
const triggerstatenode = document.querySelector<HTMLElement>("#triggerstate");
|
|
43
|
+
const mcpstatenode = document.querySelector<HTMLElement>("#mcpstate");
|
|
44
|
+
const modelstatenode = document.querySelector<HTMLElement>("#modelstate");
|
|
45
|
+
const swarmstatenode = document.querySelector<HTMLElement>("#swarmstate");
|
|
46
|
+
const environmentstatenode = document.querySelector<HTMLElement>("#environmentstate");
|
|
47
|
+
const perfstatenode = document.querySelector<HTMLElement>("#perfstate");
|
|
48
|
+
const securitystatenode = document.querySelector<HTMLElement>("#securitystate");
|
|
49
|
+
const securitycontrols = document.querySelector<HTMLElement>("#securitycontrols");
|
|
50
|
+
const sessiongridstatenode = document.querySelector<HTMLElement>("#sessiongridstate");
|
|
51
|
+
const sessiongridcontrols = document.querySelector<HTMLElement>("#sessiongridcontrols");
|
|
52
|
+
const environmentcontrols = document.querySelector<HTMLElement>("#environmentcontrols");
|
|
53
|
+
const workflowcontrols = document.querySelector<HTMLElement>("#workflowcontrols");
|
|
54
|
+
const windowcontrols = document.querySelector<HTMLElement>("#windowcontrols");
|
|
55
|
+
const connectbutton = document.querySelector<HTMLButtonElement>("#connect");
|
|
56
|
+
const sessionbutton = document.querySelector<HTMLButtonElement>("#session");
|
|
57
|
+
const pausebutton = document.querySelector<HTMLButtonElement>("#pause");
|
|
58
|
+
const stopbutton = document.querySelector<HTMLButtonElement>("#stop");
|
|
59
|
+
const killswitchbutton = document.querySelector<HTMLButtonElement>("#killswitch");
|
|
60
|
+
const openbutton = document.querySelector<HTMLButtonElement>("#openpanel");
|
|
61
|
+
|
|
62
|
+
function status(message: string, error = false): void {
|
|
63
|
+
if (statusnode) {
|
|
64
|
+
statusnode.textContent = message;
|
|
65
|
+
statusnode.dataset.state = error ? "error" : "ready";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function request(message: unknown): Promise<unknown> {
|
|
69
|
+
const response = (await chrome.runtime.sendMessage(message)) as { ok: boolean; value?: unknown; error?: string };
|
|
70
|
+
if (!response.ok) throw new Error(response.error);
|
|
71
|
+
return response.value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function pauselabel(paused: boolean): string {
|
|
75
|
+
return paused ? "Resume session" : "Pause session";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Renders the live optional capability set the user has granted. */
|
|
79
|
+
function rendercapabilities(report?: capabilityreport): void {
|
|
80
|
+
if (!capabilitiesnode) return;
|
|
81
|
+
if (!report) {
|
|
82
|
+
capabilitiesnode.textContent = "Capabilities unknown.";
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const granted = [
|
|
86
|
+
`tabs ${report.tabs ? "granted" : "absent"}`,
|
|
87
|
+
`downloads ${report.downloads ? "granted" : "absent"}`,
|
|
88
|
+
`clipboard read ${report.clipboardread ? "granted" : "absent"}`,
|
|
89
|
+
`clipboard write ${report.clipboardwrite ? "granted" : "absent"}`,
|
|
90
|
+
];
|
|
91
|
+
capabilitiesnode.textContent = `Optional capabilities: ${granted.join(" · ")}.`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Renders the currently held keys and the dialogs answered by the reviewed policy. */
|
|
95
|
+
function renderlivestate(holds?: keyholdstate[], dialogs?: dialogdecision[], signals?: pagesignallike): void {
|
|
96
|
+
if (!livenode) return;
|
|
97
|
+
const held = (holds ?? []).filter((hold) => hold.releasedat === undefined);
|
|
98
|
+
const heldsummary =
|
|
99
|
+
held.length > 0 ? held.map((hold) => `${hold.key}${hold.holdid ? ` (${hold.holdid})` : ""}`).join(", ") : "none";
|
|
100
|
+
const dialogsummary = (dialogs ?? []).length > 0 ? `${dialogs?.length} answered` : "none";
|
|
101
|
+
const pages =
|
|
102
|
+
signals && (signals.language !== undefined || signals.template !== undefined || signals.scrolllocked !== undefined)
|
|
103
|
+
? [
|
|
104
|
+
signals.language !== undefined ? `language ${signals.language || "unknown"}` : "",
|
|
105
|
+
signals.template !== undefined ? `template ${signals.template}` : "",
|
|
106
|
+
signals.scrolllocked !== undefined ? `scroll lock ${signals.scrolllocked ? "on" : "off"}` : "",
|
|
107
|
+
]
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join(" · ")
|
|
110
|
+
: "unknown";
|
|
111
|
+
livenode.textContent = `Held keys: ${heldsummary} · Open dialogs answered: ${dialogsummary} · Page signals: ${pages}.`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Renders the load phase, offline state, active wait profile and queued navigation targets of the session tab. */
|
|
115
|
+
function rendernavstate(state?: navstate, offline?: boolean, profile?: waitprofilerecord, queues?: navqueues): void {
|
|
116
|
+
if (!navstatenode) return;
|
|
117
|
+
const phase = state?.phase ? `load phase ${state.phase}` : "load phase unknown";
|
|
118
|
+
const offlinestate = offline === undefined ? "connectivity unknown" : offline ? "offline" : "online";
|
|
119
|
+
const wait = profile ? `wait profile ${profile.profile.signals.join("+")}` : "no wait profile";
|
|
120
|
+
const queued = queues
|
|
121
|
+
? `queued targets ${queues.prefetch + queues.batchopen} (${queues.prefetch} prefetch · ${queues.batchopen} batch open)`
|
|
122
|
+
: "no queued targets";
|
|
123
|
+
navstatenode.textContent = `Navigation: ${phase} · ${offlinestate} · ${wait} · ${queued}.`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Renders the current window layout and the quick window state buttons. */
|
|
127
|
+
function renderwindowlayout(windows?: windowshape[], badges?: tabbadge[]): void {
|
|
128
|
+
if (!windowlayoutnode || !windowcontrols) return;
|
|
129
|
+
windowlayoutnode.textContent =
|
|
130
|
+
windows === undefined
|
|
131
|
+
? "Window layout: unknown."
|
|
132
|
+
: `Window layout: ${windows.length} window${windows.length === 1 ? "" : "s"}${windows.length > 0 ? ` (${windows.map((item) => `${item.windowid}: ${item.state}${item.incognito ? " incognito" : ""}`).join(" · ")})` : ""} · ${new Set((badges ?? []).map((badge) => badge.tabid)).size} tab${new Set((badges ?? []).map((badge) => badge.tabid)).size === 1 ? "" : "s"} with active tasks.`;
|
|
133
|
+
windowcontrols.replaceChildren();
|
|
134
|
+
for (const item of (windows ?? []).slice(0, 4)) {
|
|
135
|
+
for (const state of ["maximized", "minimized", "normal"] as const) {
|
|
136
|
+
const quick = document.createElement("button");
|
|
137
|
+
quick.type = "button";
|
|
138
|
+
quick.className = "secondary";
|
|
139
|
+
quick.textContent = `${state === "normal" ? "restore" : state} ${item.windowid}`;
|
|
140
|
+
quick.addEventListener("click", () =>
|
|
141
|
+
request({ kind: "windowstate", windowid: item.windowid, state })
|
|
142
|
+
.then(() => {
|
|
143
|
+
status(`Window ${item.windowid} set to ${state}.`);
|
|
144
|
+
return restore();
|
|
145
|
+
})
|
|
146
|
+
.catch((error) => status(error instanceof Error ? error.message : String(error), true)),
|
|
147
|
+
);
|
|
148
|
+
windowcontrols.append(quick);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Renders the captcha handoff state while a plan waits for the user. */
|
|
154
|
+
function rendercaptchastate(captchas?: captchahandoff[]): void {
|
|
155
|
+
if (!captchastatenode) return;
|
|
156
|
+
const open = (captchas ?? []).find((handoff) => !handoff.resolved);
|
|
157
|
+
if (open) {
|
|
158
|
+
captchastatenode.dataset.state = "error";
|
|
159
|
+
captchastatenode.textContent = `Captcha handoff open on ${open.origin}; the plan waits until you resolve it in the review panel.`;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
delete captchastatenode.dataset.state;
|
|
163
|
+
captchastatenode.textContent = "Captcha handoff: none.";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Renders the active dataset row count and the stream state of the session. */
|
|
167
|
+
function renderdatasetstate(datasets?: dataset[], streams?: streamstate[]): void {
|
|
168
|
+
if (!datasetstatenode) return;
|
|
169
|
+
const rows = (datasets ?? []).reduce((total, datasetvalue) => total + datasetvalue.rows.length, 0);
|
|
170
|
+
const count = datasets?.length ?? 0;
|
|
171
|
+
const stream = (streams ?? []).find((item) => item.done !== true) ?? (streams ?? [])[0];
|
|
172
|
+
const streamsummary = stream
|
|
173
|
+
? ` · stream ${stream.name} chunk ${stream.chunk} of ${stream.chunks}${stream.done ? " complete" : ""}`
|
|
174
|
+
: "";
|
|
175
|
+
datasetstatenode.textContent =
|
|
176
|
+
count === 0
|
|
177
|
+
? "Datasets: none collected yet."
|
|
178
|
+
: `Datasets: ${count} collected (${rows} row${rows === 1 ? "" : "s"})${streamsummary}.`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Renders the live batch download states and the quarantined files awaiting scan verdicts. */
|
|
182
|
+
function renderdownloadstate(downloads?: downloadrecord[], quarantines?: quarantineentry[]): void {
|
|
183
|
+
if (!downloadstatenode) return;
|
|
184
|
+
const states = (downloads ?? []).filter(
|
|
185
|
+
(record) => record.state === "running" || record.state === "queued" || record.state === "paused",
|
|
186
|
+
);
|
|
187
|
+
const completed = (downloads ?? []).filter((record) => record.state === "complete").length;
|
|
188
|
+
const failed = (downloads ?? []).filter((record) => record.state === "failed").length;
|
|
189
|
+
const pending = (quarantines ?? []).filter((entry) => entry.scan === "pending").length;
|
|
190
|
+
const parts = [
|
|
191
|
+
states.length > 0 ? `${states.length} in flight (${states.map((record) => record.state).join(" · ")})` : "",
|
|
192
|
+
completed > 0 ? `${completed} completed` : "",
|
|
193
|
+
failed > 0 ? `${failed} failed` : "",
|
|
194
|
+
pending > 0 ? `${pending} quarantined awaiting scan` : "",
|
|
195
|
+
].filter(Boolean);
|
|
196
|
+
downloadstatenode.textContent =
|
|
197
|
+
(downloads ?? []).length === 0 && pending === 0
|
|
198
|
+
? "Downloads: none."
|
|
199
|
+
: `Downloads: ${parts.join(" · ") || "none active"}.`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Renders the clipboard consent state while a read waits for the user approval. */
|
|
203
|
+
function renderclipboardstate(consents?: clipboardconsentrecord[]): void {
|
|
204
|
+
if (!clipboardstatenode) return;
|
|
205
|
+
const waiting = (consents ?? []).filter((record) => record.approved === undefined);
|
|
206
|
+
if (waiting.length > 0) {
|
|
207
|
+
clipboardstatenode.dataset.state = "error";
|
|
208
|
+
clipboardstatenode.textContent = `Clipboard consent: ${waiting.length} read prompt${waiting.length === 1 ? "" : "s"} waiting for your approval in the review panel.`;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
delete clipboardstatenode.dataset.state;
|
|
212
|
+
clipboardstatenode.textContent = "Clipboard consent: none waiting.";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Renders the capture count of the active run beside the capture policy. */
|
|
216
|
+
function rendercapturestate(captures?: Array<{ id: string }>, policy?: string): void {
|
|
217
|
+
if (!capturestatenode) return;
|
|
218
|
+
const count = captures?.length ?? 0;
|
|
219
|
+
const mode = policy ?? "manual";
|
|
220
|
+
capturestatenode.textContent =
|
|
221
|
+
count === 0 ? `Captures: none (policy ${mode}).` : `Captures: ${count} of the run (policy ${mode}).`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Renders the observed request count of the active run beside the live channel and stream state. */
|
|
225
|
+
function rendernetstate(exchanges?: Array<{ id: string }>, channels?: Array<{ id: string; state: string }>): void {
|
|
226
|
+
if (!netstatenode) return;
|
|
227
|
+
const count = exchanges?.length ?? 0;
|
|
228
|
+
const live = (channels ?? []).filter((channel) => channel.state === "open" || channel.state === "connecting").length;
|
|
229
|
+
netstatenode.textContent = `Observed requests: ${count} of the run${live > 0 ? ` · ${live} live channel${live === 1 ? "" : "s"}` : ""}.`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Renders the error count captured in the active run beside pending console capture consent prompts, the debugger attached badge of devtools sessions and the profiling badge of active profile or trace instruments. */
|
|
233
|
+
function renderdebugstate(
|
|
234
|
+
timeline?: { errors?: Array<{ id: string }>; rejections?: Array<{ id: string }> },
|
|
235
|
+
consoleconsents?: consoleconsentrecord[],
|
|
236
|
+
debuggergrants?: Array<{ approved?: boolean; revokedat?: number }>,
|
|
237
|
+
cdpattached?: number,
|
|
238
|
+
profileactive?: number,
|
|
239
|
+
sourcemapconsents?: Array<{ approved?: boolean; revokedat?: number }>,
|
|
240
|
+
): void {
|
|
241
|
+
if (!debugstatenode) return;
|
|
242
|
+
const errors = timeline?.errors?.length ?? 0;
|
|
243
|
+
const rejections = timeline?.rejections?.length ?? 0;
|
|
244
|
+
const waiting = (consoleconsents ?? []).filter((consent) => consent.approved === undefined).length;
|
|
245
|
+
const debuggerwaiting = (debuggergrants ?? []).filter(
|
|
246
|
+
(grant) => grant.approved === undefined && grant.revokedat === undefined,
|
|
247
|
+
).length;
|
|
248
|
+
const attached = cdpattached ?? 0;
|
|
249
|
+
const profiling = profileactive ?? 0;
|
|
250
|
+
const sourcemapwaiting = (sourcemapconsents ?? []).filter(
|
|
251
|
+
(consent) => consent.approved === undefined && consent.revokedat === undefined,
|
|
252
|
+
).length;
|
|
253
|
+
debugstatenode.textContent =
|
|
254
|
+
errors === 0 &&
|
|
255
|
+
rejections === 0 &&
|
|
256
|
+
waiting === 0 &&
|
|
257
|
+
debuggerwaiting === 0 &&
|
|
258
|
+
attached === 0 &&
|
|
259
|
+
profiling === 0 &&
|
|
260
|
+
sourcemapwaiting === 0
|
|
261
|
+
? "Timeline: no errors captured."
|
|
262
|
+
: `Timeline: ${errors} error${errors === 1 ? "" : "s"}${rejections > 0 ? ` · ${rejections} rejection${rejections === 1 ? "" : "s"}` : ""} captured in the active run${waiting > 0 ? ` · ${waiting} console consent prompt${waiting === 1 ? "" : "s"} waiting` : ""}${debuggerwaiting > 0 ? ` · ${debuggerwaiting} debugger consent prompt${debuggerwaiting === 1 ? "" : "s"} waiting` : ""}${attached > 0 ? ` · debugger attached on ${attached} session${attached === 1 ? "" : "s"}` : ""}${profiling > 0 ? ` · profiling active on ${profiling} instrument${profiling === 1 ? "" : "s"}` : ""}${sourcemapwaiting > 0 ? ` · ${sourcemapwaiting} source map prompt${sourcemapwaiting === 1 ? "" : "s"} waiting` : ""}.`;
|
|
263
|
+
if (errors > 0 || waiting > 0 || debuggerwaiting > 0 || sourcemapwaiting > 0) debugstatenode.dataset.state = "error";
|
|
264
|
+
else delete debugstatenode.dataset.state;
|
|
265
|
+
if (attached > 0) debugstatenode.dataset.debugger = "attached";
|
|
266
|
+
else delete debugstatenode.dataset.debugger;
|
|
267
|
+
if (profiling > 0) debugstatenode.dataset.profile = "active";
|
|
268
|
+
else delete debugstatenode.dataset.profile;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Renders the active traffic rule count badge of controlled runs. */
|
|
272
|
+
function renderrulestate(activerules?: number, blocked?: number, mocked?: number): void {
|
|
273
|
+
if (!rulestatenode) return;
|
|
274
|
+
const rules = activerules ?? 0;
|
|
275
|
+
rulestatenode.textContent =
|
|
276
|
+
rules === 0
|
|
277
|
+
? "Traffic rules: none active."
|
|
278
|
+
: `Traffic rules: ${rules} active of the run${blocked !== undefined && blocked > 0 ? ` · ${blocked} blocked request${blocked === 1 ? "" : "s"}` : ""}${mocked !== undefined && mocked > 0 ? ` · ${mocked} mocked response${mocked === 1 ? "" : "s"}` : ""}.`;
|
|
279
|
+
if (rules > 0) rulestatenode.dataset.state = "controlled";
|
|
280
|
+
else delete rulestatenode.dataset.state;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Renders the media state badge: the recording indicator of active recordings beside the stored media record count. */
|
|
284
|
+
function rendercallstate(calls?: Array<{ id: string }>, fetchconsents?: Array<{ approved?: boolean }>): void {
|
|
285
|
+
if (!callstatenode) return;
|
|
286
|
+
const count = calls?.length ?? 0;
|
|
287
|
+
const waiting = (fetchconsents ?? []).filter((consent) => consent.approved === undefined).length;
|
|
288
|
+
callstatenode.textContent = `Outbound calls: ${count} of the run${waiting > 0 ? ` · ${waiting} header consent prompt${waiting === 1 ? "" : "s"} waiting` : ""}.`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function rendermediastate(
|
|
292
|
+
media?: Array<{ id: string }>,
|
|
293
|
+
recordingactive?: Array<{ id: string; kind: string }>,
|
|
294
|
+
recordingconsents?: Array<{ approved?: boolean }>,
|
|
295
|
+
): void {
|
|
296
|
+
if (!mediastatenode) return;
|
|
297
|
+
const active = (recordingactive ?? []).length;
|
|
298
|
+
const waiting = (recordingconsents ?? []).filter((record) => record.approved === undefined).length;
|
|
299
|
+
const count = media?.length ?? 0;
|
|
300
|
+
const recording =
|
|
301
|
+
active > 0
|
|
302
|
+
? `● recording ${active} active capture${active === 1 ? "" : "s"}`
|
|
303
|
+
: waiting > 0
|
|
304
|
+
? `recording consent: ${waiting} prompt${waiting === 1 ? "" : "s"} waiting`
|
|
305
|
+
: "";
|
|
306
|
+
mediastatenode.textContent = `Media: ${count} record${count === 1 ? "" : "s"}${recording ? ` · ${recording}` : ""}.`;
|
|
307
|
+
if (active > 0) mediastatenode.dataset.state = "recording";
|
|
308
|
+
else delete mediastatenode.dataset.state;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Renders the active emulation badge of the run: the layer names and the pending location consent prompts. */
|
|
312
|
+
function renderemulationstate(layers?: string[], locationconsents?: Array<{ approved?: boolean }>): void {
|
|
313
|
+
if (!emulationstatenode) return;
|
|
314
|
+
const active = layers ?? [];
|
|
315
|
+
const waiting = (locationconsents ?? []).filter((consent) => consent.approved === undefined).length;
|
|
316
|
+
emulationstatenode.textContent = `Emulation: ${active.length > 0 ? `${active.length} active layer${active.length === 1 ? "" : "s"} (${active.join(", ")})` : "no active layer"}${waiting > 0 ? ` · ${waiting} location consent prompt${waiting === 1 ? "" : "s"} waiting` : ""}.`;
|
|
317
|
+
if (active.length > 0) emulationstatenode.dataset.emulated = "active";
|
|
318
|
+
else delete emulationstatenode.dataset.emulated;
|
|
319
|
+
if (waiting > 0) emulationstatenode.dataset.state = "error";
|
|
320
|
+
else delete emulationstatenode.dataset.state;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Renders the saved session count and the auto snapshot state with its interval and count. */
|
|
324
|
+
function rendersessionstate(
|
|
325
|
+
memory?: {
|
|
326
|
+
records: Array<{ id: string; name: string }>;
|
|
327
|
+
auto?: { period: number; maxsnapshots: number; expiry: number };
|
|
328
|
+
},
|
|
329
|
+
auto?: { interval: { period: number; maxsnapshots: number; expiry: number }; count: number },
|
|
330
|
+
): void {
|
|
331
|
+
if (!sessionstatenode) return;
|
|
332
|
+
const saved = memory?.records.length ?? 0;
|
|
333
|
+
const interval = auto?.interval ?? memory?.auto;
|
|
334
|
+
sessionstatenode.textContent = `Sessions: ${saved} saved${interval !== undefined ? ` · auto snapshot every ${interval.period} ms (${auto?.count ?? 0} of ${interval.maxsnapshots})` : " · no auto interval"}.`;
|
|
335
|
+
if (interval !== undefined) sessionstatenode.dataset.auto = "active";
|
|
336
|
+
else delete sessionstatenode.dataset.auto;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Renders the active workflow state with pause and cancel buttons, the count of workflows running in the background and the active loop iteration count of the running runs. */
|
|
340
|
+
function renderworkflowstate(
|
|
341
|
+
runs?: Array<{ id: string; state: string; cursor: number; workflowid: string; dryrun?: boolean }>,
|
|
342
|
+
control?: Array<{ runid: string; kind: string; loops?: Array<{ path: string }> }>,
|
|
343
|
+
): void {
|
|
344
|
+
if (!workflowstatenode) return;
|
|
345
|
+
const all = runs ?? [];
|
|
346
|
+
const running = all.filter((run) => run.state === "running");
|
|
347
|
+
const paused = all.filter((run) => run.state === "paused");
|
|
348
|
+
const iterations = (control ?? [])
|
|
349
|
+
.filter((decision) => decision.kind === "loop" && running.some((run) => run.id === decision.runid))
|
|
350
|
+
.reduce((total, decision) => total + (decision.loops?.length ?? 0), 0);
|
|
351
|
+
workflowstatenode.textContent = `Workflows: ${running.length} running in the background${paused.length > 0 ? ` · ${paused.length} paused` : ""}${running.length > 0 && iterations > 0 ? ` · loop iteration ${iterations}` : ""}${all.length > 0 ? ` · latest ${all[0]?.state ?? ""}` : " · none composed yet"}.`;
|
|
352
|
+
if (running.length > 0) workflowstatenode.dataset.state = "active";
|
|
353
|
+
else delete workflowstatenode.dataset.state;
|
|
354
|
+
if (!workflowcontrols) return;
|
|
355
|
+
workflowcontrols.replaceChildren();
|
|
356
|
+
for (const run of [...running, ...paused]) {
|
|
357
|
+
const controls = document.createElement("span");
|
|
358
|
+
const pause = document.createElement("button");
|
|
359
|
+
pause.className = "secondary";
|
|
360
|
+
pause.textContent = run.state === "running" ? `Pause ${run.id.slice(0, 6)}` : `Resume ${run.id.slice(0, 6)}`;
|
|
361
|
+
pause.disabled = false;
|
|
362
|
+
pause.addEventListener("click", async () => {
|
|
363
|
+
try {
|
|
364
|
+
await request({ kind: run.state === "running" ? "pauseworkflowrun" : "resumeworkflowrun", runid: run.id });
|
|
365
|
+
status(
|
|
366
|
+
run.state === "running"
|
|
367
|
+
? "Workflow run paused at its checkpoint."
|
|
368
|
+
: "Workflow run resumed from its checkpoint.",
|
|
369
|
+
);
|
|
370
|
+
await restore();
|
|
371
|
+
} catch (error) {
|
|
372
|
+
status(error instanceof Error ? error.message : String(error), true);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
const cancel = document.createElement("button");
|
|
376
|
+
cancel.className = "danger";
|
|
377
|
+
cancel.textContent = `Cancel ${run.id.slice(0, 6)}`;
|
|
378
|
+
cancel.addEventListener("click", async () => {
|
|
379
|
+
try {
|
|
380
|
+
await request({ kind: "cancelworkflowrun", runid: run.id, reason: "popup cancel" });
|
|
381
|
+
status("Workflow run cancelled with its reason recorded.");
|
|
382
|
+
await restore();
|
|
383
|
+
} catch (error) {
|
|
384
|
+
status(error instanceof Error ? error.message : String(error), true);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
controls.append(pause, " ", cancel);
|
|
388
|
+
workflowcontrols.append(controls);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Renders the trigger layer: the armed rule count, the queued fires held while the session is paused and the button rule status of the toolbar click. */
|
|
393
|
+
/** Renders the mcp server running indicator of the agent protocol: the running state, the localhost bind, the connected clients, the paired count, the pending approval gates as notifications and the long tool progress of the in flight calls that announces itself while the panel stays hidden. */
|
|
394
|
+
/** Renders the active model per running task kind: every routed task kind shows its provider and model so the user reads which model drives which task. */
|
|
395
|
+
function rendermodelstate(state?: {
|
|
396
|
+
providers: Array<{ id: string; name: string; status: string }>;
|
|
397
|
+
tasks: Array<{ kind: string; provider?: string; model?: string }>;
|
|
398
|
+
usage: { totaltokens: number; cost: number; calls: number };
|
|
399
|
+
}): void {
|
|
400
|
+
if (!modelstatenode) return;
|
|
401
|
+
if (!state) {
|
|
402
|
+
modelstatenode.textContent = "Models: none connected.";
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const routed = state.tasks.filter((task) => task.provider !== undefined);
|
|
406
|
+
modelstatenode.textContent = `Models: ${
|
|
407
|
+
routed.length > 0
|
|
408
|
+
? routed
|
|
409
|
+
.slice(0, 3)
|
|
410
|
+
.map((task) => `${task.kind} ${task.model}`)
|
|
411
|
+
.join(" · ")
|
|
412
|
+
: "no route yet"
|
|
413
|
+
} · ${state.usage.calls} call${state.usage.calls === 1 ? "" : "s"} · ${state.usage.totaltokens} tokens.`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Renders the swarm summary: the popup reads the agent count with the active, paused and stopped split, the claimed tasks of the shared queue, the elected leader and the held lock count, so the user reads the swarm at a glance. */
|
|
417
|
+
/** Renders the execution environment status of the running session: the environments of the open run, the keepalive heartbeat indicator, the offscreen capability state and the zombie warning with its reap action. */
|
|
418
|
+
function renderenvironmentstate(view?: {
|
|
419
|
+
report: {
|
|
420
|
+
environments: Array<{ stepid: string; environment: string }>;
|
|
421
|
+
workers: number;
|
|
422
|
+
keepalive?: { runid: string; state: string; beats: number; lastbeatat: number; portopen: boolean };
|
|
423
|
+
};
|
|
424
|
+
offscreengranted: boolean;
|
|
425
|
+
parseoffload: boolean;
|
|
426
|
+
zombies: string[];
|
|
427
|
+
grants?: string[];
|
|
428
|
+
}): void {
|
|
429
|
+
if (!environmentstatenode) return;
|
|
430
|
+
if (!view) {
|
|
431
|
+
environmentstatenode.textContent = "Environments: no run state.";
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const counted = new Map<string, number>();
|
|
435
|
+
for (const entry of view.report.environments)
|
|
436
|
+
counted.set(entry.environment, (counted.get(entry.environment) ?? 0) + 1);
|
|
437
|
+
const breakdown = [...counted.entries()].map(([environment, count]) => `${environment} ${count}`).join(" · ");
|
|
438
|
+
const keepalive = view.report.keepalive;
|
|
439
|
+
environmentstatenode.textContent = `Environments: ${breakdown.length > 0 ? breakdown : "no step executed"} · offscreen ${view.offscreengranted ? "granted" : "absent"} · offload ${view.parseoffload ? "on" : "off"}${view.report.workers > 0 ? ` · workers ${view.report.workers}` : ""} · keepalive ${keepalive ? `${keepalive.state === "active" && keepalive.portopen ? "♥" : "closed"} ${keepalive.beats} beat${keepalive.beats === 1 ? "" : "s"}` : "none"}${view.zombies.length > 0 ? ` · zombie runs ${view.zombies.join(", ")}` : ""}${view.grants !== undefined ? ` · grants ${view.grants.length > 0 ? view.grants.join(", ") : "default"}` : ""}.`;
|
|
440
|
+
if (keepalive?.state === "active" && keepalive.portopen) environmentstatenode.dataset.state = "active";
|
|
441
|
+
else delete environmentstatenode.dataset.state;
|
|
442
|
+
if (environmentcontrols) {
|
|
443
|
+
environmentcontrols.replaceChildren();
|
|
444
|
+
if (view.zombies.length > 0) {
|
|
445
|
+
const reap = document.createElement("button");
|
|
446
|
+
reap.className = "secondary";
|
|
447
|
+
reap.textContent = "Reap zombie runs";
|
|
448
|
+
reap.addEventListener("click", () => {
|
|
449
|
+
void request({ kind: "runstate", reap: true })
|
|
450
|
+
.then(() => restore())
|
|
451
|
+
.catch((error) => status(error instanceof Error ? error.message : String(error), true));
|
|
452
|
+
});
|
|
453
|
+
environmentcontrols.append(reap);
|
|
454
|
+
}
|
|
455
|
+
if (!view.offscreengranted) {
|
|
456
|
+
const grant = document.createElement("button");
|
|
457
|
+
grant.className = "secondary";
|
|
458
|
+
grant.textContent = "Request offscreen grant";
|
|
459
|
+
grant.addEventListener("click", () => {
|
|
460
|
+
void request({ kind: "environments", requestcapability: true })
|
|
461
|
+
.then(() => restore())
|
|
462
|
+
.catch((error) => status(error instanceof Error ? error.message : String(error), true));
|
|
463
|
+
});
|
|
464
|
+
environmentcontrols.append(grant);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Renders the security posture of the active tab: the origin allowlist state, the originprofile summary, the consent window state, the denydefault notice and the safedefaults notice of an unprofiled origin that links to the transparencypage originprofile editor, with the allowlist grant request for a new origin. */
|
|
470
|
+
function rendersecuritystate(
|
|
471
|
+
view?: {
|
|
472
|
+
posture: string;
|
|
473
|
+
allowlist: Array<{ origin: string; profileid: string }>;
|
|
474
|
+
profiles: Array<{ origin: string; grants: string[]; denials: string[] }>;
|
|
475
|
+
windows: Array<{ origin: string; state: string; expiresat: number }>;
|
|
476
|
+
sessionorigin?: string;
|
|
477
|
+
chain: Array<{ runid: string; valid: boolean; entries: number; sealhash?: string }>;
|
|
478
|
+
},
|
|
479
|
+
activeorigin?: string,
|
|
480
|
+
): void {
|
|
481
|
+
if (!securitystatenode) return;
|
|
482
|
+
if (!view) {
|
|
483
|
+
securitystatenode.textContent = "Security: state unknown.";
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const origin = activeorigin ?? view.sessionorigin ?? "the active tab";
|
|
487
|
+
const granted = view.allowlist.some((entry) => entry.origin === origin);
|
|
488
|
+
const profile = view.profiles.find((candidate) => candidate.origin === origin);
|
|
489
|
+
const windowstate = view.windows.find((candidate) => candidate.origin === origin && candidate.state === "active");
|
|
490
|
+
const verified = view.chain.filter((chain) => chain.valid).length;
|
|
491
|
+
securitystatenode.textContent = `Security: ${origin} ${granted ? "allowlisted" : "ungranted (denydefault)"}${profile !== undefined ? ` · profile ${profile.grants.length} granted / ${profile.denials.length} denied` : " · no origin profile"}${windowstate !== undefined ? ` · consent window until ${new Date(windowstate.expiresat).toLocaleTimeString()}` : " · no active consent window"} · ${verified}/${view.chain.length} log chain${view.chain.length === 1 ? "" : "s"} verified.`;
|
|
492
|
+
if (granted) delete securitystatenode.dataset.state;
|
|
493
|
+
else securitystatenode.dataset.state = "error";
|
|
494
|
+
if (securitycontrols) {
|
|
495
|
+
securitycontrols.replaceChildren();
|
|
496
|
+
if (!granted) {
|
|
497
|
+
const grant = document.createElement("button");
|
|
498
|
+
grant.className = "secondary";
|
|
499
|
+
grant.textContent = `Grant ${origin} allowlist entry`;
|
|
500
|
+
grant.addEventListener("click", () => {
|
|
501
|
+
void request({ kind: "security", allowlist: { add: { origin } } })
|
|
502
|
+
.then(() => restore())
|
|
503
|
+
.catch((error) => status(error instanceof Error ? error.message : String(error), true));
|
|
504
|
+
});
|
|
505
|
+
securitycontrols.append(grant);
|
|
506
|
+
}
|
|
507
|
+
if (profile === undefined && origin !== "the active tab") {
|
|
508
|
+
const safedefaults = document.createElement("button");
|
|
509
|
+
safedefaults.className = "secondary";
|
|
510
|
+
safedefaults.textContent = `Safedefaults on ${origin}: reads only — open the originprofile editor`;
|
|
511
|
+
safedefaults.addEventListener("click", () => {
|
|
512
|
+
try {
|
|
513
|
+
chrome.runtime.openOptionsPage(() => {
|
|
514
|
+
void chrome.runtime.lastError;
|
|
515
|
+
});
|
|
516
|
+
} catch {
|
|
517
|
+
status("The transparencypage did not open; open it from the extensions page.", true);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
securitycontrols.append(safedefaults);
|
|
521
|
+
}
|
|
522
|
+
if (view.chain.length > 0) {
|
|
523
|
+
const seal = view.chain.find((chain) => chain.sealhash !== undefined);
|
|
524
|
+
if (seal?.sealhash !== undefined) {
|
|
525
|
+
const sealinfo = document.createElement("span");
|
|
526
|
+
sealinfo.className = "muted";
|
|
527
|
+
sealinfo.textContent = `Seal ${seal.sealhash.slice(0, 10)}…`;
|
|
528
|
+
securitycontrols.append(sealinfo);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function renderswarmstate(state?: {
|
|
535
|
+
overview: { agents: number; active: number; paused: number; stopped: number; claimed: number; queued: number };
|
|
536
|
+
killswitch: { engaged: boolean };
|
|
537
|
+
topology?: { leaderid: string };
|
|
538
|
+
locks?: Array<{ key: string }>;
|
|
539
|
+
}): void {
|
|
540
|
+
if (!swarmstatenode) return;
|
|
541
|
+
if (!state) {
|
|
542
|
+
swarmstatenode.textContent = "Swarm: no agents.";
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
swarmstatenode.textContent = `Swarm: ${state.overview.agents} agent${state.overview.agents === 1 ? "" : "s"}${state.overview.agents > 0 ? ` (${state.overview.active} active${state.overview.paused > 0 ? `, ${state.overview.paused} paused` : ""}${state.overview.stopped > 0 ? `, ${state.overview.stopped} stopped` : ""})` : ""} · ${state.overview.claimed} active task${state.overview.claimed === 1 ? "" : "s"}${state.overview.queued > 0 ? ` · ${state.overview.queued} queued` : ""}${state.topology !== undefined ? ` · leader ${state.topology.leaderid}` : ""}${state.locks !== undefined && state.locks.length > 0 ? ` · ${state.locks.length} lock${state.locks.length === 1 ? "" : "s"}` : ""}${state.killswitch.engaged ? " · killswitch engaged" : ""}.`;
|
|
546
|
+
if (state.overview.active > 0 || state.killswitch.engaged) swarmstatenode.dataset.state = "active";
|
|
547
|
+
else delete swarmstatenode.dataset.state;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function rendermcpstate(state?: {
|
|
551
|
+
state: string;
|
|
552
|
+
bind?: string;
|
|
553
|
+
port: number;
|
|
554
|
+
localhost: boolean;
|
|
555
|
+
clients?: Array<{ id: string; paired: boolean }>;
|
|
556
|
+
approvals?: Array<{ id: string; tool: string; state: string }>;
|
|
557
|
+
inflight?: Array<{ callid: string; tool: string; chunks: number }>;
|
|
558
|
+
progressnotices?: Array<{ callid: string; percent?: number; message: string; cancellable?: boolean; at: number }>;
|
|
559
|
+
}): void {
|
|
560
|
+
if (!mcpstatenode) return;
|
|
561
|
+
const running = state?.state === "running";
|
|
562
|
+
const clients = state?.clients ?? [];
|
|
563
|
+
const paired = clients.filter((client) => client.paired).length;
|
|
564
|
+
const pending = (state?.approvals ?? []).filter((approval) => approval.state === "pending");
|
|
565
|
+
const inflight = state?.inflight ?? [];
|
|
566
|
+
const notice = (state?.progressnotices ?? [])[0];
|
|
567
|
+
mcpstatenode.textContent = `Mcp server: ${running ? `running on ${state?.bind ?? "127.0.0.1"}:${state?.port ?? 7436}${state?.localhost === false ? " (remote bind)" : ""} · ${clients.length} client${clients.length === 1 ? "" : "s"}${paired !== clients.length ? ` · ${paired} paired` : ""}${pending.length > 0 ? ` · ${pending.length} approval gate${pending.length === 1 ? "" : "s"} waiting (${[...new Set(pending.map((approval) => approval.tool))].slice(0, 3).join(", ")})` : ""}${inflight.length > 0 ? ` · ${inflight.length} call${inflight.length === 1 ? "" : "s"} in flight${notice !== undefined ? ` (${notice.percent !== undefined ? `${notice.percent}%` : notice.message}${notice.cancellable === false ? "" : ", cancellable in the panel"})` : ""}` : ""}` : (state?.state ?? "stopped")}.`;
|
|
568
|
+
if (running) mcpstatenode.dataset.state = "active";
|
|
569
|
+
else delete mcpstatenode.dataset.state;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function rendertriggerstate(
|
|
573
|
+
rules?: Array<{ id: string; kind: string; enabled: boolean; paused?: boolean; nextfireat?: number }>,
|
|
574
|
+
queued?: number,
|
|
575
|
+
paused?: boolean,
|
|
576
|
+
buttonfired?: boolean,
|
|
577
|
+
): void {
|
|
578
|
+
if (!triggerstatenode) return;
|
|
579
|
+
const armed = rules ?? [];
|
|
580
|
+
const enabled = armed.filter((rule) => rule.enabled && rule.paused !== true);
|
|
581
|
+
const scheduled = enabled.filter((rule) => rule.nextfireat !== undefined);
|
|
582
|
+
const buttons = enabled.filter((rule) => rule.kind === "button");
|
|
583
|
+
triggerstatenode.textContent = `Triggers: ${armed.length} armed rule${armed.length === 1 ? "" : "s"}${enabled.length !== armed.length ? ` · ${enabled.length} enabled` : ""}${scheduled.length > 0 ? ` · ${scheduled.length} scheduled` : ""}${(queued ?? 0) > 0 ? ` · ${queued} fire${queued === 1 ? "" : "s"} ${paused ? "held while the session is paused" : "queued"}` : ""}${buttons.length > 0 ? ` · toolbar button ${buttonfired ? "fired" : "ready"}` : ""}.`;
|
|
584
|
+
if (enabled.length > 0) triggerstatenode.dataset.state = "active";
|
|
585
|
+
else delete triggerstatenode.dataset.state;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/** Renders the 1.1.68 performance state of the popup: the active worker queue depth with the deferred count the backpressure holds and the startup module budget of the user prewarm set. */
|
|
589
|
+
/** Renders the run heartbeat staleness of the long task: the last beat, its age against the user window and the zombie risk the popup warns about. */
|
|
590
|
+
async function renderheartbeatstate(): Promise<void> {
|
|
591
|
+
const node = document.querySelector<HTMLElement>("#heartbeatstate");
|
|
592
|
+
if (!node) return;
|
|
593
|
+
try {
|
|
594
|
+
const view = (await request({ kind: "resilience", view: true })) as {
|
|
595
|
+
runs: Array<{ runid: string; state: string; updatedat: number; heartbeat?: number; beats?: number }>;
|
|
596
|
+
window: number;
|
|
597
|
+
};
|
|
598
|
+
const running = view.runs.filter((run) => run.state === "running");
|
|
599
|
+
if (running.length === 0) {
|
|
600
|
+
node.textContent = "Heartbeat: no running run heartbeats yet.";
|
|
601
|
+
delete node.dataset.state;
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const newest = running.reduce((one, two) => ((two.heartbeat ?? 0) > (one.heartbeat ?? 0) ? two : one));
|
|
605
|
+
const beats = newest.beats ?? 0;
|
|
606
|
+
const age = newest.heartbeat !== undefined ? Math.max(0, Date.now() - newest.heartbeat) : undefined;
|
|
607
|
+
const stale = age !== undefined && age > view.window;
|
|
608
|
+
node.textContent = `Heartbeat: run ${newest.runid} beat ${beats} time${beats === 1 ? "" : "s"}${age !== undefined ? ` · last beat ${Math.round(age / 1000)}s ago` : " · no beat recorded"} of the ${Math.round(view.window / 1000)}s window${stale ? " · stale, the zombiecheck may reap the run" : ""}.`;
|
|
609
|
+
if (stale) node.dataset.state = "stale";
|
|
610
|
+
else delete node.dataset.state;
|
|
611
|
+
} catch {
|
|
612
|
+
node.textContent = "Heartbeat: no run heartbeats yet.";
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** Renders the at rest encryption status of the memory workspace: whether encryptrest stays on, how many stored items encrypt and whether every provenance stands complete for the export. */
|
|
617
|
+
async function renderencryptstate(): Promise<void> {
|
|
618
|
+
const node = document.querySelector<HTMLElement>("#encryptstate");
|
|
619
|
+
if (!node) return;
|
|
620
|
+
try {
|
|
621
|
+
const view = (await request({ kind: "state", view: true })) as {
|
|
622
|
+
memory: { items: number; encrypted: number; provenancecomplete: boolean };
|
|
623
|
+
encryptrest: boolean;
|
|
624
|
+
};
|
|
625
|
+
node.textContent = `Encryption: encryptrest ${view.encryptrest ? "on" : "off"} · ${view.memory.encrypted} of ${view.memory.items} item${view.memory.items === 1 ? "" : "s"} encrypted${view.encryptrest ? " through the webcrypto derived key; the key never persists and the secret entry stays a consent prompt" : "; sensitive classes refuse plaintext writes once the user turns it on"}${view.memory.provenancecomplete ? " · provenance complete for the export" : " · provenance incomplete for the export"}.`;
|
|
626
|
+
if (view.encryptrest) node.dataset.state = "encrypted";
|
|
627
|
+
else delete node.dataset.state;
|
|
628
|
+
} catch {
|
|
629
|
+
node.textContent = "Encryption: no memory workspace yet.";
|
|
630
|
+
delete node.dataset.state;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Renders the fleet state of the 1.1.72 family: the agent count with its control states, the open escalations and reviews and the timestamp of the last killswitch stop. */
|
|
635
|
+
async function renderfleetstate(): Promise<void> {
|
|
636
|
+
const node = document.querySelector<HTMLElement>("#fleetstate");
|
|
637
|
+
if (!node) return;
|
|
638
|
+
try {
|
|
639
|
+
const view = (await request({ kind: "fleet", view: true })) as {
|
|
640
|
+
overview: {
|
|
641
|
+
agents: number;
|
|
642
|
+
active: number;
|
|
643
|
+
paused: number;
|
|
644
|
+
stopped: number;
|
|
645
|
+
openescalations: number;
|
|
646
|
+
openreviews: number;
|
|
647
|
+
lastkillswitchat?: number;
|
|
648
|
+
};
|
|
649
|
+
};
|
|
650
|
+
node.textContent = `Fleet: ${view.overview.agents} agent${view.overview.agents === 1 ? "" : "s"} (${view.overview.active} active, ${view.overview.paused} paused, ${view.overview.stopped} stopped)${view.overview.openescalations > 0 ? ` · ${view.overview.openescalations} open escalation${view.overview.openescalations === 1 ? "" : "s"}` : ""}${view.overview.openreviews > 0 ? ` · ${view.overview.openreviews} open review${view.overview.openreviews === 1 ? "" : "s"}` : ""}${view.overview.lastkillswitchat !== undefined ? ` · last killswitch ${new Date(view.overview.lastkillswitchat).toISOString()}` : " · killswitch never engaged"}.`;
|
|
651
|
+
if (view.overview.paused > 0) node.dataset.state = "paused";
|
|
652
|
+
else delete node.dataset.state;
|
|
653
|
+
} catch {
|
|
654
|
+
node.textContent = "Fleet: no agents.";
|
|
655
|
+
delete node.dataset.state;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** Renders the worker state of the 1.1.73 family: the live worker count per origin the scaleworkers pass reads, so the popup shows where the fleet stands. */
|
|
660
|
+
async function renderworkstate(): Promise<void> {
|
|
661
|
+
const node = document.querySelector<HTMLElement>("#workstate");
|
|
662
|
+
if (!node) return;
|
|
663
|
+
try {
|
|
664
|
+
const view = (await request({ kind: "work", view: true })) as {
|
|
665
|
+
workersperorigin: Array<{ origin: string; count: number }>;
|
|
666
|
+
suggestions: Array<{ origin: string; suggestion: string; reason: string }>;
|
|
667
|
+
};
|
|
668
|
+
if (view.workersperorigin.length === 0) {
|
|
669
|
+
node.textContent = "Workers: none.";
|
|
670
|
+
delete node.dataset.state;
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
node.textContent = `Workers: ${view.workersperorigin.map((entry) => `${entry.count} on ${entry.origin}`).join(" · ")}${view.suggestions.some((suggestion) => suggestion.suggestion !== "hold") ? ` · ${view.suggestions.filter((suggestion) => suggestion.suggestion !== "hold").length} scale suggestion${view.suggestions.filter((suggestion) => suggestion.suggestion !== "hold").length === 1 ? "" : "s"} in the sidepanel` : ""}.`;
|
|
674
|
+
if (view.workersperorigin.some((entry) => entry.count > 0)) node.dataset.state = "active";
|
|
675
|
+
else delete node.dataset.state;
|
|
676
|
+
} catch {
|
|
677
|
+
node.textContent = "Workers: none.";
|
|
678
|
+
delete node.dataset.state;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function renderpipelinestate(): Promise<void> {
|
|
683
|
+
const node = document.querySelector<HTMLElement>("#pipelinestate");
|
|
684
|
+
if (!node) return;
|
|
685
|
+
try {
|
|
686
|
+
const view = (await request({ kind: "pipeline", view: true })) as {
|
|
687
|
+
state: { running: number; paused: number; finished: number };
|
|
688
|
+
progress: Array<{ rows: number; bytes: number; state: string }>;
|
|
689
|
+
};
|
|
690
|
+
const active = view.state.running + view.state.paused;
|
|
691
|
+
if (active === 0 && view.state.finished === 0) {
|
|
692
|
+
node.textContent = "Pipelines: none.";
|
|
693
|
+
delete node.dataset.state;
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
node.textContent = `Pipelines: ${view.state.running} running · ${view.state.paused} paused · ${view.state.finished} finished${
|
|
697
|
+
view.progress.some((entry) => entry.state !== "finished")
|
|
698
|
+
? ` · ${view.progress
|
|
699
|
+
.filter((entry) => entry.state !== "finished")
|
|
700
|
+
.reduce((total, entry) => total + entry.rows, 0)
|
|
701
|
+
.toLocaleString()} row${view.progress.filter((entry) => entry.state !== "finished").reduce((total, entry) => total + entry.rows, 0) === 1 ? "" : "s"} streamed`
|
|
702
|
+
: ""
|
|
703
|
+
}.`;
|
|
704
|
+
if (active > 0) node.dataset.state = "active";
|
|
705
|
+
else delete node.dataset.state;
|
|
706
|
+
} catch {
|
|
707
|
+
node.textContent = "Pipelines: none.";
|
|
708
|
+
delete node.dataset.state;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function rendertransportstate(): Promise<void> {
|
|
713
|
+
const node = document.querySelector<HTMLElement>("#transportstate");
|
|
714
|
+
if (!node) return;
|
|
715
|
+
try {
|
|
716
|
+
const view = (await request({ kind: "webapi", view: true })) as {
|
|
717
|
+
subscriptions: Array<{ state: string }>;
|
|
718
|
+
graphqlchannels: Array<{ completed: boolean }>;
|
|
719
|
+
ratelimits: Array<{ origin: string }>;
|
|
720
|
+
cache: Array<{ hits: number }>;
|
|
721
|
+
};
|
|
722
|
+
const open =
|
|
723
|
+
view.subscriptions.filter((record) => record.state === "open").length +
|
|
724
|
+
view.graphqlchannels.filter((channel) => !channel.completed).length;
|
|
725
|
+
const hits = view.cache.reduce((total, entry) => total + entry.hits, 0);
|
|
726
|
+
if (open === 0 && view.ratelimits.length === 0 && hits === 0) {
|
|
727
|
+
node.textContent = "Transports: none open.";
|
|
728
|
+
delete node.dataset.state;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
node.textContent = `Transports: ${open} open${view.ratelimits.length > 0 ? ` · ${view.ratelimits.length} rate directive${view.ratelimits.length === 1 ? "" : "s"}` : ""}${hits > 0 ? ` · ${hits} cache hit${hits === 1 ? "" : "s"}` : ""}.`;
|
|
732
|
+
if (open > 0) node.dataset.state = "active";
|
|
733
|
+
else delete node.dataset.state;
|
|
734
|
+
} catch {
|
|
735
|
+
node.textContent = "Transports: none open.";
|
|
736
|
+
delete node.dataset.state;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** Renders the popup vision model status of the 1.1.77 family: the configured model with the ocr, description and cache hit counts of the run, or the unconfigured refusal because no recognition ships inside the extension. */
|
|
741
|
+
async function rendervisionstate(): Promise<void> {
|
|
742
|
+
const node = document.querySelector<HTMLElement>("#visionstate");
|
|
743
|
+
if (!node) return;
|
|
744
|
+
try {
|
|
745
|
+
const view = (await request({ kind: "vision", view: true })) as {
|
|
746
|
+
ocrs: Array<{ words: number }>;
|
|
747
|
+
visions: Array<{ regions: number }>;
|
|
748
|
+
cachehits: number;
|
|
749
|
+
cost: { modelcalls: number; calls: number };
|
|
750
|
+
settings: { visionmodel?: string; visionendpoint?: string };
|
|
751
|
+
};
|
|
752
|
+
const words = view.ocrs.reduce((total, record) => total + record.words, 0);
|
|
753
|
+
if (view.ocrs.length === 0 && view.visions.length === 0) {
|
|
754
|
+
node.textContent =
|
|
755
|
+
view.settings.visionmodel !== undefined
|
|
756
|
+
? `Vision: model ${view.settings.visionmodel} configured, none read yet.`
|
|
757
|
+
: "Vision: no model configured.";
|
|
758
|
+
delete node.dataset.state;
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
node.textContent = `Vision: ${view.settings.visionmodel !== undefined ? view.settings.visionmodel : "unconfigured model"} · ${view.ocrs.length} ocr result${view.ocrs.length === 1 ? "" : "s"} of ${words} word${words === 1 ? "" : "s"} · ${view.visions.length} description${view.visions.length === 1 ? "" : "s"} · ${view.cachehits} cache hit${view.cachehits === 1 ? "" : "s"} · ${view.cost.modelcalls} model call${view.cost.modelcalls === 1 ? "" : "s"}.`;
|
|
762
|
+
if (view.cost.modelcalls > 0) node.dataset.state = "active";
|
|
763
|
+
else delete node.dataset.state;
|
|
764
|
+
} catch {
|
|
765
|
+
node.textContent = "Vision: no model configured.";
|
|
766
|
+
delete node.dataset.state;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** Renders the popup forensic evidence status of the 1.1.78 family: the before after pair, console, net and regression counts of the run with the latest diff score, so the popup names the evidence the run gathered at a glance. */
|
|
771
|
+
async function renderforensicsstate(): Promise<void> {
|
|
772
|
+
const node = document.querySelector<HTMLElement>("#forensicsstate");
|
|
773
|
+
if (!node) return;
|
|
774
|
+
try {
|
|
775
|
+
const view = (await request({ kind: "forensics", view: true })) as {
|
|
776
|
+
pairs: Array<{ stepkind: string }>;
|
|
777
|
+
consoleentries: Array<{ level: string }>;
|
|
778
|
+
netentries: Array<{ status: number }>;
|
|
779
|
+
diffs: Array<{ regression: boolean; score: number }>;
|
|
780
|
+
lastdiffscore?: number;
|
|
781
|
+
};
|
|
782
|
+
const errors = view.consoleentries.filter((entry) => entry.level === "error").length;
|
|
783
|
+
const regressions = view.diffs.filter((diff) => diff.regression).length;
|
|
784
|
+
if (
|
|
785
|
+
view.pairs.length === 0 &&
|
|
786
|
+
view.consoleentries.length === 0 &&
|
|
787
|
+
view.netentries.length === 0 &&
|
|
788
|
+
view.diffs.length === 0
|
|
789
|
+
) {
|
|
790
|
+
node.textContent = "Forensics: none.";
|
|
791
|
+
delete node.dataset.state;
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
node.textContent = `Forensics: ${view.pairs.length} pair${view.pairs.length === 1 ? "" : "s"} · ${view.consoleentries.length} console entr${view.consoleentries.length === 1 ? "y" : "ies"} (${errors} error${errors === 1 ? "" : "s"}) · ${view.netentries.length} net entr${view.netentries.length === 1 ? "y" : "ies"} · ${regressions} regression${regressions === 1 ? "" : "s"}${view.lastdiffscore !== undefined ? ` · last diff ${view.lastdiffscore}` : ""}.`;
|
|
795
|
+
if (errors > 0 || regressions > 0) node.dataset.state = "active";
|
|
796
|
+
else delete node.dataset.state;
|
|
797
|
+
} catch {
|
|
798
|
+
node.textContent = "Forensics: none.";
|
|
799
|
+
delete node.dataset.state;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** Renders the popup telemetry status of the 1.1.79 minimization family: the policy stays fixed to off by default and by construction — the enabled literal of the telemetrypolicy type makes an on state unrepresentable — so the popup names the posture and every counter keeps living inside the local memory. */
|
|
804
|
+
async function renderbridgestate(): Promise<void> {
|
|
805
|
+
const node = document.querySelector<HTMLElement>("#bridgestate");
|
|
806
|
+
if (!node) return;
|
|
807
|
+
try {
|
|
808
|
+
const view = (await request({ kind: "bridge", view: true })) as {
|
|
809
|
+
status: { status: string; paired: boolean; queued: number };
|
|
810
|
+
};
|
|
811
|
+
node.textContent = bridgestatuslabel(view.status);
|
|
812
|
+
if (view.status.status === "connected") node.dataset.state = "active";
|
|
813
|
+
else delete node.dataset.state;
|
|
814
|
+
} catch {
|
|
815
|
+
node.textContent = "Bridge: disabled (no relay url set).";
|
|
816
|
+
delete node.dataset.state;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async function rendertelemetrystate(): Promise<void> {
|
|
821
|
+
const node = document.querySelector<HTMLElement>("#telemetrystate");
|
|
822
|
+
if (!node) return;
|
|
823
|
+
try {
|
|
824
|
+
const view = (await request({ kind: "minimization", view: true })) as {
|
|
825
|
+
telemetry: { enabled: boolean; counters: string };
|
|
826
|
+
inventory: Array<{ key: string; dataclass: string; size: number; records: number }>;
|
|
827
|
+
};
|
|
828
|
+
const localsize = view.inventory.reduce((total, entry) => total + entry.size, 0);
|
|
829
|
+
node.textContent = `Telemetry: ${view.telemetry.enabled === false ? "off" : "off"} (fixed) · ${view.telemetry.counters} counters · ${view.inventory.length} stored famil${view.inventory.length === 1 ? "y" : "ies"} of ${localsize} byte${localsize === 1 ? "" : "s"} stay on the device.`;
|
|
830
|
+
delete node.dataset.state;
|
|
831
|
+
} catch {
|
|
832
|
+
node.textContent = "Telemetry: off by default and by construction.";
|
|
833
|
+
delete node.dataset.state;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async function renderperfstate(): Promise<void> {
|
|
838
|
+
if (!perfstatenode) return;
|
|
839
|
+
try {
|
|
840
|
+
const view = (await request({ kind: "perf", view: true })) as {
|
|
841
|
+
queue: { depth: number; deferred: number; peak: number; configured?: number };
|
|
842
|
+
startup: { prewarmed: string[]; lazy: string[]; budget?: number; over: boolean };
|
|
843
|
+
};
|
|
844
|
+
perfstatenode.textContent = `Performance: worker queue ${view.queue.depth} pending${view.queue.deferred > 0 ? ` · ${view.queue.deferred} deferred` : ""}${view.queue.peak > 0 ? ` · peak ${view.queue.peak}` : ""}${view.queue.configured !== undefined ? ` · user depth ${view.queue.configured}` : ""} · startup ${view.startup.prewarmed.length} prewarmed of ${view.startup.prewarmed.length + view.startup.lazy.length} module${view.startup.prewarmed.length + view.startup.lazy.length === 1 ? "" : "s"}${view.startup.budget !== undefined ? ` · budget ${view.startup.budget}${view.startup.over ? " (over, reported never refused)" : ""}` : ""}.`;
|
|
845
|
+
if (view.queue.depth > 0) perfstatenode.dataset.state = "active";
|
|
846
|
+
else delete perfstatenode.dataset.state;
|
|
847
|
+
} catch {
|
|
848
|
+
perfstatenode.textContent = "Performance: no run measured.";
|
|
849
|
+
delete perfstatenode.dataset.state;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async function restore(): Promise<void> {
|
|
854
|
+
const context = (await request({ kind: "context" })) as {
|
|
855
|
+
config?: { endpoint: string };
|
|
856
|
+
session?: { stoppedat?: number; pausedat?: number; expiresat: number; origin?: string };
|
|
857
|
+
capabilities?: capabilityreport;
|
|
858
|
+
holds?: { heldkeys: keyholdstate[] };
|
|
859
|
+
dialogs?: dialogdecision[];
|
|
860
|
+
signals?: pagesignallike;
|
|
861
|
+
navstate?: navstate;
|
|
862
|
+
offline?: boolean;
|
|
863
|
+
waitprofile?: waitprofilerecord;
|
|
864
|
+
navqueues?: navqueues;
|
|
865
|
+
windows?: windowshape[];
|
|
866
|
+
badges?: tabbadge[];
|
|
867
|
+
captchas?: captchahandoff[];
|
|
868
|
+
datasets?: dataset[];
|
|
869
|
+
streams?: streamstate[];
|
|
870
|
+
downloads?: downloadrecord[];
|
|
871
|
+
clipconsents?: clipboardconsentrecord[];
|
|
872
|
+
quarantines?: quarantineentry[];
|
|
873
|
+
captures?: Array<{ id: string }>;
|
|
874
|
+
capturepolicy?: string;
|
|
875
|
+
media?: Array<{ id: string }>;
|
|
876
|
+
recordingactive?: Array<{ id: string; kind: string }>;
|
|
877
|
+
recordingconsents?: Array<{ approved?: boolean }>;
|
|
878
|
+
calls?: Array<{ id: string }>;
|
|
879
|
+
fetchconsents?: Array<{ approved?: boolean }>;
|
|
880
|
+
exchanges?: Array<{ id: string }>;
|
|
881
|
+
channels?: Array<{ id: string; state: string }>;
|
|
882
|
+
activerules?: number;
|
|
883
|
+
timeline?: { errors?: Array<{ id: string }>; rejections?: Array<{ id: string }> };
|
|
884
|
+
consoleconsents?: consoleconsentrecord[];
|
|
885
|
+
debuggergrants?: Array<{ approved?: boolean; revokedat?: number }>;
|
|
886
|
+
cdpattached?: number;
|
|
887
|
+
profileactive?: number;
|
|
888
|
+
profile?: { consents?: Array<{ approved?: boolean; revokedat?: number }> };
|
|
889
|
+
emulatedlayers?: string[];
|
|
890
|
+
emulation?: { consents: Array<{ approved?: boolean }> };
|
|
891
|
+
traffic?: {
|
|
892
|
+
blocks: Array<{ hits: number; revertedat?: number }>;
|
|
893
|
+
mocks: Array<{ hits: number; revertedat?: number }>;
|
|
894
|
+
};
|
|
895
|
+
sessionmemory?: {
|
|
896
|
+
records: Array<{ id: string; name: string }>;
|
|
897
|
+
auto?: { period: number; maxsnapshots: number; expiry: number };
|
|
898
|
+
};
|
|
899
|
+
autosnapshotstate?: { interval: { period: number; maxsnapshots: number; expiry: number }; count: number };
|
|
900
|
+
workflow?: {
|
|
901
|
+
runs: Array<{ id: string; state: string; cursor: number; workflowid: string; dryrun?: boolean }>;
|
|
902
|
+
control?: Array<{ runid: string; kind: string; loops?: Array<{ path: string }> }>;
|
|
903
|
+
};
|
|
904
|
+
trigger?: {
|
|
905
|
+
rules: Array<{ id: string; kind: string; enabled: boolean; paused?: boolean; nextfireat?: number }>;
|
|
906
|
+
queued: number;
|
|
907
|
+
};
|
|
908
|
+
mcp?: {
|
|
909
|
+
state: string;
|
|
910
|
+
bind?: string;
|
|
911
|
+
port: number;
|
|
912
|
+
localhost: boolean;
|
|
913
|
+
clients: Array<{ id: string; paired: boolean }>;
|
|
914
|
+
approvals: Array<{ id: string; tool: string; state: string }>;
|
|
915
|
+
inflight?: Array<{ callid: string; tool: string; chunks: number }>;
|
|
916
|
+
progressnotices?: Array<{ callid: string; percent?: number; message: string; cancellable?: boolean; at: number }>;
|
|
917
|
+
};
|
|
918
|
+
llm?: {
|
|
919
|
+
providers: Array<{ id: string; name: string; status: string }>;
|
|
920
|
+
tasks: Array<{ kind: string; provider?: string; model?: string }>;
|
|
921
|
+
usage: { totaltokens: number; cost: number; calls: number };
|
|
922
|
+
};
|
|
923
|
+
swarm?: {
|
|
924
|
+
overview: { agents: number; active: number; paused: number; stopped: number; claimed: number; queued: number };
|
|
925
|
+
killswitch: { engaged: boolean };
|
|
926
|
+
topology?: { leaderid: string };
|
|
927
|
+
locks?: Array<{ key: string }>;
|
|
928
|
+
};
|
|
929
|
+
environments?: {
|
|
930
|
+
report: {
|
|
931
|
+
environments: Array<{ stepid: string; environment: string }>;
|
|
932
|
+
workers: number;
|
|
933
|
+
keepalive?: { runid: string; state: string; beats: number; lastbeatat: number; portopen: boolean };
|
|
934
|
+
};
|
|
935
|
+
offscreengranted: boolean;
|
|
936
|
+
parseoffload: boolean;
|
|
937
|
+
zombies: string[];
|
|
938
|
+
grants?: string[];
|
|
939
|
+
};
|
|
940
|
+
security?: {
|
|
941
|
+
posture: string;
|
|
942
|
+
allowlist: Array<{ origin: string; profileid: string }>;
|
|
943
|
+
profiles: Array<{ origin: string; grants: string[]; denials: string[] }>;
|
|
944
|
+
windows: Array<{ origin: string; state: string; expiresat: number }>;
|
|
945
|
+
sessionorigin?: string;
|
|
946
|
+
chain: Array<{ runid: string; valid: boolean; entries: number; sealhash?: string }>;
|
|
947
|
+
};
|
|
948
|
+
sessionview?: {
|
|
949
|
+
grid: Array<{
|
|
950
|
+
sessionid: string;
|
|
951
|
+
runid: string;
|
|
952
|
+
origins: string[];
|
|
953
|
+
state: string;
|
|
954
|
+
outcome: string;
|
|
955
|
+
steps: number;
|
|
956
|
+
completed: number;
|
|
957
|
+
lock: string;
|
|
958
|
+
tabid?: number;
|
|
959
|
+
sealhash?: string;
|
|
960
|
+
updatedat: number;
|
|
961
|
+
actions: string[];
|
|
962
|
+
}>;
|
|
963
|
+
};
|
|
964
|
+
v1sunset?: { at: number; declared: number; clientid: string; dismissedat?: number };
|
|
965
|
+
migrationprompt?: {
|
|
966
|
+
promptedat: number;
|
|
967
|
+
previousversion: string;
|
|
968
|
+
command: string;
|
|
969
|
+
migrationpromptdismissed?: boolean;
|
|
970
|
+
dismissedat?: number;
|
|
971
|
+
};
|
|
972
|
+
};
|
|
973
|
+
if (endpointinput && context.config) endpointinput.value = context.config.endpoint;
|
|
974
|
+
rendercapabilities(context.capabilities);
|
|
975
|
+
renderlivestate(context.holds?.heldkeys, context.dialogs, context.signals);
|
|
976
|
+
rendernavstate(context.navstate, context.offline, context.waitprofile, context.navqueues);
|
|
977
|
+
renderwindowlayout(context.windows, context.badges);
|
|
978
|
+
rendercaptchastate(context.captchas);
|
|
979
|
+
renderdatasetstate(context.datasets, context.streams);
|
|
980
|
+
renderdownloadstate(context.downloads, context.quarantines);
|
|
981
|
+
renderclipboardstate(context.clipconsents);
|
|
982
|
+
rendercapturestate(context.captures, context.capturepolicy);
|
|
983
|
+
rendermediastate(context.media, context.recordingactive, context.recordingconsents);
|
|
984
|
+
rendercallstate(context.calls, context.fetchconsents);
|
|
985
|
+
rendernetstate(context.exchanges, context.channels);
|
|
986
|
+
renderdebugstate(
|
|
987
|
+
context.timeline,
|
|
988
|
+
context.consoleconsents,
|
|
989
|
+
context.debuggergrants,
|
|
990
|
+
context.cdpattached,
|
|
991
|
+
context.profileactive,
|
|
992
|
+
context.profile?.consents,
|
|
993
|
+
);
|
|
994
|
+
renderemulationstate(context.emulatedlayers, context.emulation?.consents);
|
|
995
|
+
rendersessionstate(context.sessionmemory, context.autosnapshotstate);
|
|
996
|
+
renderworkflowstate(context.workflow?.runs, context.workflow?.control);
|
|
997
|
+
rendertriggerstate(
|
|
998
|
+
context.trigger?.rules,
|
|
999
|
+
context.trigger?.queued,
|
|
1000
|
+
context.session?.pausedat !== undefined,
|
|
1001
|
+
triggerbuttonfired,
|
|
1002
|
+
);
|
|
1003
|
+
rendermcpstate(context.mcp);
|
|
1004
|
+
rendermodelstate(context.llm);
|
|
1005
|
+
renderswarmstate(context.swarm);
|
|
1006
|
+
renderenvironmentstate(context.environments);
|
|
1007
|
+
await renderperfstate();
|
|
1008
|
+
await renderheartbeatstate();
|
|
1009
|
+
await renderencryptstate();
|
|
1010
|
+
await renderfleetstate();
|
|
1011
|
+
await renderworkstate();
|
|
1012
|
+
await renderpipelinestate();
|
|
1013
|
+
await rendertransportstate();
|
|
1014
|
+
await rendervisionstate();
|
|
1015
|
+
await renderforensicsstate();
|
|
1016
|
+
await rendertelemetrystate();
|
|
1017
|
+
await renderbridgestate();
|
|
1018
|
+
rendersecuritystate(context.security, context.session?.origin);
|
|
1019
|
+
renderv1sunsetbanner(context.v1sunset);
|
|
1020
|
+
rendermigrationpromptbanner(context.migrationprompt);
|
|
1021
|
+
rendersessiongridstate(context.sessionview?.grid);
|
|
1022
|
+
renderrulestate(
|
|
1023
|
+
context.activerules,
|
|
1024
|
+
(context.traffic?.blocks ?? [])
|
|
1025
|
+
.filter((rule) => rule.revertedat === undefined)
|
|
1026
|
+
.reduce((total, rule) => total + rule.hits, 0),
|
|
1027
|
+
(context.traffic?.mocks ?? [])
|
|
1028
|
+
.filter((spec) => spec.revertedat === undefined)
|
|
1029
|
+
.reduce((total, spec) => total + spec.hits, 0),
|
|
1030
|
+
);
|
|
1031
|
+
const active = context.session && !context.session.stoppedat && context.session.expiresat > Date.now();
|
|
1032
|
+
if (pausebutton) {
|
|
1033
|
+
pausebutton.disabled = !active;
|
|
1034
|
+
pausebutton.textContent = pauselabel(Boolean(context.session?.pausedat));
|
|
1035
|
+
}
|
|
1036
|
+
if (active && context.session?.pausedat) status("Session paused. Reviewed actions are blocked until resume.");
|
|
1037
|
+
else status(active ? "Session active. Review the plan in the side panel." : "No active browser session.");
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/** The popup open is the toolbar button click of a default popup: an armed button rule evaluates behind the gates once per popup open. */
|
|
1041
|
+
let triggerbuttonfired = false;
|
|
1042
|
+
void (async () => {
|
|
1043
|
+
try {
|
|
1044
|
+
const result = (await request({ kind: "buttontrigger" })) as { fired: boolean; reason?: string };
|
|
1045
|
+
if (result.fired) {
|
|
1046
|
+
triggerbuttonfired = true;
|
|
1047
|
+
status("The toolbar button rule fired behind the consent gates.");
|
|
1048
|
+
}
|
|
1049
|
+
} catch {
|
|
1050
|
+
/* no armed button rule or the gates refused the fire; the panel shows the reason */
|
|
1051
|
+
}
|
|
1052
|
+
})();
|
|
1053
|
+
|
|
1054
|
+
connectbutton?.addEventListener("click", async () => {
|
|
1055
|
+
try {
|
|
1056
|
+
const config = normalizeendpoint(endpointinput?.value ?? "");
|
|
1057
|
+
const granted = await chrome.permissions.request({ origins: [hostpattern(config.origin)] });
|
|
1058
|
+
if (!granted) throw new Error("Origin permission was not granted.");
|
|
1059
|
+
await request({ kind: "configure", endpoint: config.endpoint });
|
|
1060
|
+
status(`Endpoint approved for ${config.origin}.`);
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
status(error instanceof Error ? error.message : String(error), true);
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
sessionbutton?.addEventListener("click", async () => {
|
|
1066
|
+
try {
|
|
1067
|
+
await request({ kind: "startsession" });
|
|
1068
|
+
status("Session started for the active HTTPS tab.");
|
|
1069
|
+
await restore();
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
status(error instanceof Error ? error.message : String(error), true);
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
pausebutton?.addEventListener("click", async () => {
|
|
1075
|
+
try {
|
|
1076
|
+
const context = (await request({ kind: "context" })) as { session?: { pausedat?: number } };
|
|
1077
|
+
await request({ kind: context.session?.pausedat ? "resumesession" : "pausesession" });
|
|
1078
|
+
await restore();
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
status(error instanceof Error ? error.message : String(error), true);
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
stopbutton?.addEventListener("click", async () => {
|
|
1084
|
+
try {
|
|
1085
|
+
await request({ kind: "stop" });
|
|
1086
|
+
status("Session stopped. No action can continue.");
|
|
1087
|
+
await restore();
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
status(error instanceof Error ? error.message : String(error), true);
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
killswitchbutton?.addEventListener("click", async () => {
|
|
1093
|
+
try {
|
|
1094
|
+
await request({ kind: "fleet", killswitch: { reason: "The popup killswitch button." } });
|
|
1095
|
+
status(
|
|
1096
|
+
"The killswitch engaged: every fleet record stopped, every agent run cancelled and every queue cleared; the switch stays user triggered only.",
|
|
1097
|
+
);
|
|
1098
|
+
await restore();
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
status(error instanceof Error ? error.message : String(error), true);
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
openbutton?.addEventListener("click", () => chrome.sidePanel.open({ windowId: chrome.windows.WINDOW_ID_CURRENT }));
|
|
1104
|
+
restore().catch((error) => status(error instanceof Error ? error.message : String(error), true));
|
|
1105
|
+
|
|
1106
|
+
/** Renders the compact sessiongrid of recent sessions: every live and saved run in one row with its state, outcome and lock, and a deep link that opens the sidepanel run view. */
|
|
1107
|
+
function rendersessiongridstate(
|
|
1108
|
+
grid?: Array<{
|
|
1109
|
+
sessionid: string;
|
|
1110
|
+
runid: string;
|
|
1111
|
+
origins: string[];
|
|
1112
|
+
state: string;
|
|
1113
|
+
outcome: string;
|
|
1114
|
+
steps: number;
|
|
1115
|
+
completed: number;
|
|
1116
|
+
lock: string;
|
|
1117
|
+
tabid?: number;
|
|
1118
|
+
sealhash?: string;
|
|
1119
|
+
updatedat: number;
|
|
1120
|
+
actions: string[];
|
|
1121
|
+
}>,
|
|
1122
|
+
): void {
|
|
1123
|
+
if (!sessiongridstatenode) return;
|
|
1124
|
+
if (!grid || grid.length === 0) {
|
|
1125
|
+
sessiongridstatenode.textContent = "Session grid: no run yet.";
|
|
1126
|
+
if (sessiongridcontrols) sessiongridcontrols.replaceChildren();
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const recent = grid.slice(0, 3);
|
|
1130
|
+
sessiongridstatenode.textContent = `Session grid: ${grid.length} run${grid.length === 1 ? "" : "s"} — ${recent.map((row) => `${row.state} ${row.runid.slice(0, 8)} ${row.completed}/${row.steps} ${row.lock}`).join(" · ")}.`;
|
|
1131
|
+
if (!sessiongridcontrols) return;
|
|
1132
|
+
sessiongridcontrols.replaceChildren();
|
|
1133
|
+
for (const row of recent) {
|
|
1134
|
+
const deep = document.createElement("button");
|
|
1135
|
+
deep.type = "button";
|
|
1136
|
+
deep.className = "secondary";
|
|
1137
|
+
deep.textContent = `Open ${row.state} run ${row.runid.slice(0, 8)}`;
|
|
1138
|
+
deep.addEventListener("click", () =>
|
|
1139
|
+
request({ kind: "sessions", grid: { open: { runid: row.runid } } })
|
|
1140
|
+
.then(() => {
|
|
1141
|
+
status(`The session grid row of the run ${row.runid} opened in the sidepanel run view.`);
|
|
1142
|
+
return restore();
|
|
1143
|
+
})
|
|
1144
|
+
.catch((error) => status(error instanceof Error ? error.message : String(error), true)),
|
|
1145
|
+
);
|
|
1146
|
+
sessiongridcontrols.append(deep);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/** Command surface of the 1.1.64 family: the popup carries the task input, the quick run actions, the command palette with its keyboard shortcut, the onboarding walkthrough and the live broadcast refresh. */
|
|
1151
|
+
const taskinputnode = document.querySelector<HTMLTextAreaElement>("#taskinput");
|
|
1152
|
+
const tasksubmitbutton = document.querySelector<HTMLButtonElement>("#tasksubmit");
|
|
1153
|
+
const proposalstatusnode = document.querySelector<HTMLElement>("#proposalstatus");
|
|
1154
|
+
const runquickbutton = document.querySelector<HTMLButtonElement>("#runquick");
|
|
1155
|
+
const cancelquickbutton = document.querySelector<HTMLButtonElement>("#cancelquick");
|
|
1156
|
+
const opendashboardbutton = document.querySelector<HTMLButtonElement>("#opendashboard");
|
|
1157
|
+
const openoptionsbutton = document.querySelector<HTMLButtonElement>("#openoptions");
|
|
1158
|
+
const paletteinput = document.querySelector<HTMLInputElement>("#paletteinput");
|
|
1159
|
+
const paletteopenbutton = document.querySelector<HTMLButtonElement>("#paletteopen");
|
|
1160
|
+
const palettematchesnode = document.querySelector<HTMLElement>("#palettematches");
|
|
1161
|
+
const onboardingcardnode = document.querySelector<HTMLElement>("#onboardingcard");
|
|
1162
|
+
|
|
1163
|
+
/** The single broadcast channel every surface subscribes to: the popup refreshes its live status from each frame. */
|
|
1164
|
+
const surfacechannel: BroadcastChannel | undefined =
|
|
1165
|
+
typeof BroadcastChannel === "function" ? new BroadcastChannel("devthinksurfaces") : undefined;
|
|
1166
|
+
surfacechannel?.addEventListener("message", () => {
|
|
1167
|
+
void restore().catch(() => {
|
|
1168
|
+
/* a failing refresh keeps the last rendered state */
|
|
1169
|
+
});
|
|
1170
|
+
void renderonboarding().catch(() => {
|
|
1171
|
+
/* a failing walkthrough render keeps the card as it was */
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
/** Shows the proposal status of the taskinput while the plan generates. */
|
|
1176
|
+
function proposalstatus(status: "idle" | "generating" | "ready" | "failed", detail = ""): void {
|
|
1177
|
+
if (!proposalstatusnode) return;
|
|
1178
|
+
proposalstatusnode.textContent = detail === "" ? status : `${status} — ${detail}`;
|
|
1179
|
+
proposalstatusnode.dataset.state = status;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/** Submits the taskinput goal through the same proposal flow as the api with the active origin and page outline attached. */
|
|
1183
|
+
tasksubmitbutton?.addEventListener("click", async () => {
|
|
1184
|
+
const text = taskinputnode?.value ?? "";
|
|
1185
|
+
if (text.trim() === "") {
|
|
1186
|
+
status("Describe the goal first; the proposal flow needs its natural language task.", true);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
proposalstatus("generating");
|
|
1190
|
+
try {
|
|
1191
|
+
const result = (await request({ kind: "surface", task: { submit: { text, surface: "popup" } } })) as {
|
|
1192
|
+
plan?: { id: string; state: string };
|
|
1193
|
+
status?: string;
|
|
1194
|
+
};
|
|
1195
|
+
proposalstatus("ready", `plan ${result.plan?.id ?? ""} awaits its plancard review`);
|
|
1196
|
+
status(`The taskinput goal became the plan ${result.plan?.id ?? ""}; review its plancards in the side panel.`);
|
|
1197
|
+
await restore();
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
proposalstatus("failed", error instanceof Error ? error.message : String(error));
|
|
1200
|
+
status(error instanceof Error ? error.message : String(error), true);
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
/** Runs the quick actions of the command surface through the command bus. */
|
|
1205
|
+
async function busaction(command: string, payload = "", stepid?: string): Promise<void> {
|
|
1206
|
+
const action = {
|
|
1207
|
+
surface: "popup",
|
|
1208
|
+
command,
|
|
1209
|
+
...(payload !== "" ? { payload } : {}),
|
|
1210
|
+
...(stepid !== undefined ? { stepid } : {}),
|
|
1211
|
+
};
|
|
1212
|
+
const route = (await request({ kind: "surface", bus: { action } })) as {
|
|
1213
|
+
route: { dispatched: boolean; reason: string };
|
|
1214
|
+
};
|
|
1215
|
+
status(route.route.reason);
|
|
1216
|
+
await restore();
|
|
1217
|
+
}
|
|
1218
|
+
runquickbutton?.addEventListener("click", () => {
|
|
1219
|
+
void busaction("starttask", taskinputnode?.value ?? "").catch((error) =>
|
|
1220
|
+
status(error instanceof Error ? error.message : String(error), true),
|
|
1221
|
+
);
|
|
1222
|
+
});
|
|
1223
|
+
cancelquickbutton?.addEventListener("click", () => {
|
|
1224
|
+
void busaction("cancelrun").catch((error) => status(error instanceof Error ? error.message : String(error), true));
|
|
1225
|
+
});
|
|
1226
|
+
opendashboardbutton?.addEventListener("click", () => {
|
|
1227
|
+
void chrome.tabs.create({ url: chrome.runtime.getURL("dashboardpage.html") });
|
|
1228
|
+
});
|
|
1229
|
+
openoptionsbutton?.addEventListener("click", () => {
|
|
1230
|
+
void chrome.tabs.create({ url: chrome.runtime.getURL("optionspage.html") });
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
/** Renders the fuzzy palette matches with the recent commands ranked first. */
|
|
1234
|
+
async function renderpalette(): Promise<void> {
|
|
1235
|
+
if (!palettematchesnode) return;
|
|
1236
|
+
const text = paletteinput?.value ?? "";
|
|
1237
|
+
try {
|
|
1238
|
+
const result = (await request({ kind: "surface", palette: { query: { text } } })) as {
|
|
1239
|
+
matches: Array<{
|
|
1240
|
+
entry: { id: string; label: string; action: { command: string; surface: string } };
|
|
1241
|
+
reason: string;
|
|
1242
|
+
}>;
|
|
1243
|
+
};
|
|
1244
|
+
palettematchesnode.replaceChildren();
|
|
1245
|
+
for (const match of result.matches.slice(0, 6)) {
|
|
1246
|
+
const item = document.createElement("button");
|
|
1247
|
+
item.type = "button";
|
|
1248
|
+
item.className = "secondary";
|
|
1249
|
+
item.textContent = `${match.entry.label} (${match.entry.action.surface})`;
|
|
1250
|
+
item.title = match.reason;
|
|
1251
|
+
item.addEventListener("click", () => {
|
|
1252
|
+
void runpalettecommand(match.entry.action.command).catch((error) =>
|
|
1253
|
+
status(error instanceof Error ? error.message : String(error), true),
|
|
1254
|
+
);
|
|
1255
|
+
});
|
|
1256
|
+
palettematchesnode.append(item, " ");
|
|
1257
|
+
}
|
|
1258
|
+
if (result.matches.length === 0)
|
|
1259
|
+
palettematchesnode.textContent =
|
|
1260
|
+
"No command matches; the palette lists only the actions the current capability set allows.";
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
palettematchesnode.textContent = error instanceof Error ? error.message : String(error);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/** Runs one commandpalette command: the navigation commands open their surface while every other command rides the command bus and its use records for the ranking. */
|
|
1267
|
+
async function runpalettecommand(command: string): Promise<void> {
|
|
1268
|
+
await request({ kind: "surface", palette: { used: { command } } });
|
|
1269
|
+
if (command === "opendashboardpage") {
|
|
1270
|
+
await chrome.tabs.create({ url: chrome.runtime.getURL("dashboardpage.html") });
|
|
1271
|
+
status("The dashboardpage opened in a new tab.");
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
if (command === "openoptionspage") {
|
|
1275
|
+
await chrome.tabs.create({ url: chrome.runtime.getURL("optionspage.html") });
|
|
1276
|
+
status("The optionspage opened in a new tab.");
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (command === "opentransparencypage") {
|
|
1280
|
+
await chrome.tabs.create({ url: chrome.runtime.getURL("transparencypage.html") });
|
|
1281
|
+
status("The transparencypage opened in a new tab.");
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (command === "replayonboarding") {
|
|
1285
|
+
await request({ kind: "surface", onboarding: { replay: true } });
|
|
1286
|
+
await renderonboarding();
|
|
1287
|
+
status("The onboarding walkthrough replays.");
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
if (command === "historysearch") {
|
|
1291
|
+
await chrome.tabs.create({ url: chrome.runtime.getURL("dashboardpage.html") });
|
|
1292
|
+
status("The dashboardpage opened with the historysearch view.");
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
if (command === "copyauditexcerpt") {
|
|
1296
|
+
const result = (await request({ kind: "surface", logstream: { excerpt: {} } })) as { excerpt: string };
|
|
1297
|
+
await navigator.clipboard?.writeText(result.excerpt).catch(() => {
|
|
1298
|
+
/* the clipboard write needs its reviewed clipboard consent */
|
|
1299
|
+
});
|
|
1300
|
+
status("The verified range copied as an audit excerpt.");
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
await busaction(command);
|
|
1304
|
+
await renderpalette();
|
|
1305
|
+
}
|
|
1306
|
+
paletteinput?.addEventListener("input", () => {
|
|
1307
|
+
void renderpalette();
|
|
1308
|
+
});
|
|
1309
|
+
paletteopenbutton?.addEventListener("click", () => {
|
|
1310
|
+
paletteinput?.focus();
|
|
1311
|
+
void renderpalette();
|
|
1312
|
+
});
|
|
1313
|
+
document.addEventListener("keydown", (event) => {
|
|
1314
|
+
const shortcut = event.ctrlKey && event.key === ".";
|
|
1315
|
+
if (!shortcut) return;
|
|
1316
|
+
event.preventDefault();
|
|
1317
|
+
paletteinput?.focus();
|
|
1318
|
+
void renderpalette();
|
|
1319
|
+
});
|
|
1320
|
+
void renderpalette();
|
|
1321
|
+
|
|
1322
|
+
/** Renders the onboarding walkthrough card while the walkthrough runs: the current step with its body and its completion button. */
|
|
1323
|
+
async function renderonboarding(): Promise<void> {
|
|
1324
|
+
if (!onboardingcardnode) return;
|
|
1325
|
+
try {
|
|
1326
|
+
const result = (await request({ kind: "surface", onboarding: {} })) as {
|
|
1327
|
+
steps?: Array<{ id: string; surface: string; title: string; body: string; completion: string }>;
|
|
1328
|
+
state?: { stepscompleted: string[]; done: boolean };
|
|
1329
|
+
};
|
|
1330
|
+
const steps = result.steps ?? [];
|
|
1331
|
+
const state = result.state;
|
|
1332
|
+
if (!state || state.done || steps.length === 0) {
|
|
1333
|
+
onboardingcardnode.hidden = true;
|
|
1334
|
+
onboardingcardnode.replaceChildren();
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const current = steps.find((step) => !state.stepscompleted.includes(step.id)) ?? steps[0];
|
|
1338
|
+
if (current === undefined) {
|
|
1339
|
+
onboardingcardnode.hidden = true;
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
onboardingcardnode.hidden = false;
|
|
1343
|
+
onboardingcardnode.replaceChildren();
|
|
1344
|
+
const title = document.createElement("strong");
|
|
1345
|
+
title.textContent = `Onboarding — ${current.title} (${state.stepscompleted.length}/${steps.length})`;
|
|
1346
|
+
const body = document.createElement("p");
|
|
1347
|
+
body.textContent = current.body;
|
|
1348
|
+
const done = document.createElement("button");
|
|
1349
|
+
done.type = "button";
|
|
1350
|
+
done.textContent = `Mark ${current.title} done`;
|
|
1351
|
+
done.addEventListener("click", () => {
|
|
1352
|
+
void request({ kind: "surface", onboarding: { complete: { stepid: current.id } } })
|
|
1353
|
+
.then(() => renderonboarding())
|
|
1354
|
+
.then(() => restore())
|
|
1355
|
+
.catch((error) => status(error instanceof Error ? error.message : String(error), true));
|
|
1356
|
+
});
|
|
1357
|
+
onboardingcardnode.append(title, body, done);
|
|
1358
|
+
} catch {
|
|
1359
|
+
onboardingcardnode.hidden = true;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
void renderonboarding();
|
|
1363
|
+
|
|
1364
|
+
/** The version one sunset notice record the context carries: the time the first below floor declaration arrived, the major the client declared, the client that declared it and the session dismissal time. */
|
|
1365
|
+
type v1sunsetnotice = { at: number; declared: number; clientid: string; dismissedat?: number };
|
|
1366
|
+
|
|
1367
|
+
/** The version one migration prompt record the context carries: the time the prompt fired, the 1.x release the update upgraded from, the migrateplan command it surfaces and the persistent dismissal flag. */
|
|
1368
|
+
type migrationpromptnotice = {
|
|
1369
|
+
promptedat: number;
|
|
1370
|
+
previousversion: string;
|
|
1371
|
+
command: string;
|
|
1372
|
+
migrationpromptdismissed?: boolean;
|
|
1373
|
+
dismissedat?: number;
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
/** Builds the version one sunset banner card content: the plain language sunset notice the negotiation refusal texts carry, the migration guide path the banner references, the migration command the card names and the visibility the session state decides. */
|
|
1377
|
+
export function v1sunsetbannercard(state: v1sunsetnotice | undefined): {
|
|
1378
|
+
title: string;
|
|
1379
|
+
body: string;
|
|
1380
|
+
guide: string;
|
|
1381
|
+
command: string;
|
|
1382
|
+
dismiss: string;
|
|
1383
|
+
visible: boolean;
|
|
1384
|
+
} {
|
|
1385
|
+
return {
|
|
1386
|
+
title: "Protocol v1 retired",
|
|
1387
|
+
body: "Protocol v1 retired — this client cannot connect. The migration guide and the migrateplan command upgrade v1 plans.",
|
|
1388
|
+
guide: "docs/migrationguide.md",
|
|
1389
|
+
command: "devthink migrateplan",
|
|
1390
|
+
dismiss: "Dismiss for this session",
|
|
1391
|
+
visible: state !== undefined && state.dismissedat === undefined,
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/** Builds the version one migration prompt card content: the one time upgrade notice for an affected upgrader, the guide path, the migrateplan command the Migrate action surfaces and the visibility the persistent dismissal flag decides. */
|
|
1396
|
+
export function migrationpromptcard(state: migrationpromptnotice | undefined): {
|
|
1397
|
+
title: string;
|
|
1398
|
+
body: string;
|
|
1399
|
+
guide: string;
|
|
1400
|
+
command: string;
|
|
1401
|
+
migrate: string;
|
|
1402
|
+
dismiss: string;
|
|
1403
|
+
visible: boolean;
|
|
1404
|
+
} {
|
|
1405
|
+
return {
|
|
1406
|
+
title: "Version one migration",
|
|
1407
|
+
body: `The update from ${state?.previousversion ?? "a version one release"} closed the protocol v1 window; convert v1 plans before the next connection.`,
|
|
1408
|
+
guide: "docs/migrationguide.md",
|
|
1409
|
+
command: state?.command ?? "devthink migrateplan",
|
|
1410
|
+
migrate: "Migrate",
|
|
1411
|
+
dismiss: "Dismiss permanently",
|
|
1412
|
+
visible: state !== undefined && state.migrationpromptdismissed !== true && state.dismissedat === undefined,
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/** The lazily created banner cards the popup appends to its body: the design file carries no dedicated sunset node, so the popup builds the two cards it needs — the version one sunset banner and the version one migration prompt — and removes them when their state hides. */
|
|
1417
|
+
const sunsetbannersection = typeof document === "undefined" ? undefined : document.createElement("section");
|
|
1418
|
+
const migrationbannersection = typeof document === "undefined" ? undefined : document.createElement("section");
|
|
1419
|
+
|
|
1420
|
+
/** Renders the version one sunset banner card: the notice names the retired protocol, the guide path and the migration command, and the dismissal resets it for the session through the surface command family. */
|
|
1421
|
+
export function renderv1sunsetbanner(state: v1sunsetnotice | undefined): void {
|
|
1422
|
+
if (sunsetbannersection === undefined || typeof document.body?.append !== "function") return;
|
|
1423
|
+
const card = v1sunsetbannercard(state);
|
|
1424
|
+
if (!card.visible) {
|
|
1425
|
+
sunsetbannersection.remove();
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
sunsetbannersection.className = "panel";
|
|
1429
|
+
sunsetbannersection.setAttribute("role", "status");
|
|
1430
|
+
sunsetbannersection.replaceChildren();
|
|
1431
|
+
const title = document.createElement("strong");
|
|
1432
|
+
title.textContent = card.title;
|
|
1433
|
+
const body = document.createElement("p");
|
|
1434
|
+
body.textContent = `${card.body} Guide: ${card.guide}. Command: ${card.command}.`;
|
|
1435
|
+
const dismiss = document.createElement("button");
|
|
1436
|
+
dismiss.type = "button";
|
|
1437
|
+
dismiss.className = "secondary";
|
|
1438
|
+
dismiss.textContent = card.dismiss;
|
|
1439
|
+
dismiss.addEventListener("click", () => {
|
|
1440
|
+
void request({ kind: "surface", v1sunset: { dismiss: true } })
|
|
1441
|
+
.then(() => renderv1sunsetbanner(undefined))
|
|
1442
|
+
.catch((error) => status(error instanceof Error ? error.message : String(error), true));
|
|
1443
|
+
});
|
|
1444
|
+
sunsetbannersection.append(title, body, dismiss);
|
|
1445
|
+
document.body.append(sunsetbannersection);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/** Renders the version one migration prompt card: the Migrate action surfaces the migrateplan command the prompt record carries, and the dismissal writes the persistent flag through the surface command family so the prompt never appears again. */
|
|
1449
|
+
export function rendermigrationpromptbanner(state: migrationpromptnotice | undefined): void {
|
|
1450
|
+
if (migrationbannersection === undefined || typeof document.body?.append !== "function") return;
|
|
1451
|
+
const card = migrationpromptcard(state);
|
|
1452
|
+
if (!card.visible) {
|
|
1453
|
+
migrationbannersection.remove();
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
migrationbannersection.className = "panel";
|
|
1457
|
+
migrationbannersection.setAttribute("role", "status");
|
|
1458
|
+
migrationbannersection.replaceChildren();
|
|
1459
|
+
const title = document.createElement("strong");
|
|
1460
|
+
title.textContent = card.title;
|
|
1461
|
+
const body = document.createElement("p");
|
|
1462
|
+
body.textContent = `${card.body} Guide: ${card.guide}.`;
|
|
1463
|
+
const migrate = document.createElement("button");
|
|
1464
|
+
migrate.type = "button";
|
|
1465
|
+
migrate.className = "secondary";
|
|
1466
|
+
migrate.textContent = card.migrate;
|
|
1467
|
+
migrate.addEventListener("click", () => {
|
|
1468
|
+
void request({ kind: "surface", migrationprompt: { migrate: true } })
|
|
1469
|
+
.then(() => status(`The migration command: ${card.command}. The guide at ${card.guide} records the full path.`))
|
|
1470
|
+
.catch((error) => status(error instanceof Error ? error.message : String(error), true));
|
|
1471
|
+
});
|
|
1472
|
+
const dismiss = document.createElement("button");
|
|
1473
|
+
dismiss.type = "button";
|
|
1474
|
+
dismiss.className = "secondary";
|
|
1475
|
+
dismiss.textContent = card.dismiss;
|
|
1476
|
+
dismiss.addEventListener("click", () => {
|
|
1477
|
+
void request({ kind: "surface", migrationprompt: { dismiss: true } })
|
|
1478
|
+
.then(() => rendermigrationpromptbanner(undefined))
|
|
1479
|
+
.catch((error) => status(error instanceof Error ? error.message : String(error), true));
|
|
1480
|
+
});
|
|
1481
|
+
migrationbannersection.append(title, body, migrate, " ", dismiss);
|
|
1482
|
+
document.body.append(migrationbannersection);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/** Recenttray of the 1.1.65 family: the popup lists the latest runs with resume for a halted run and reopen for a completed one through the same sessions seam. */
|
|
1486
|
+
const recenttraynode = document.querySelector<HTMLElement>("#recenttray");
|
|
1487
|
+
async function renderrecenttray(): Promise<void> {
|
|
1488
|
+
if (!recenttraynode) return;
|
|
1489
|
+
try {
|
|
1490
|
+
const result = (await request({ kind: "views", recent: { list: true } })) as {
|
|
1491
|
+
tray?: Array<{
|
|
1492
|
+
runid: string;
|
|
1493
|
+
origin: string;
|
|
1494
|
+
outcome: string;
|
|
1495
|
+
title: string;
|
|
1496
|
+
at: number;
|
|
1497
|
+
resumable: boolean;
|
|
1498
|
+
reopenable: boolean;
|
|
1499
|
+
}>;
|
|
1500
|
+
};
|
|
1501
|
+
const tray = result.tray ?? [];
|
|
1502
|
+
recenttraynode.replaceChildren();
|
|
1503
|
+
if (tray.length === 0)
|
|
1504
|
+
recenttraynode.textContent = "No run yet; the tray lists the latest runs with resume and reopen.";
|
|
1505
|
+
else {
|
|
1506
|
+
const list = document.createElement("ul");
|
|
1507
|
+
list.className = "audit";
|
|
1508
|
+
for (const entry of tray) {
|
|
1509
|
+
const item = document.createElement("li");
|
|
1510
|
+
item.textContent = `${entry.outcome} — ${entry.title || entry.runid} (${entry.origin})`;
|
|
1511
|
+
if (entry.resumable) {
|
|
1512
|
+
const resume = document.createElement("button");
|
|
1513
|
+
resume.type = "button";
|
|
1514
|
+
resume.className = "secondary";
|
|
1515
|
+
resume.textContent = "Resume";
|
|
1516
|
+
resume.addEventListener("click", () => {
|
|
1517
|
+
void (async () => {
|
|
1518
|
+
await request({ kind: "sessions", grid: { resume: { runid: entry.runid } } });
|
|
1519
|
+
status(`The run ${entry.runid} resumed through the sessions seam.`);
|
|
1520
|
+
})().catch((error) => status(error instanceof Error ? error.message : String(error), true));
|
|
1521
|
+
});
|
|
1522
|
+
item.append(" ", resume);
|
|
1523
|
+
}
|
|
1524
|
+
if (entry.reopenable) {
|
|
1525
|
+
const reopen = document.createElement("button");
|
|
1526
|
+
reopen.type = "button";
|
|
1527
|
+
reopen.className = "secondary";
|
|
1528
|
+
reopen.textContent = "Reopen";
|
|
1529
|
+
reopen.addEventListener("click", () => {
|
|
1530
|
+
void (async () => {
|
|
1531
|
+
await request({ kind: "sessions", grid: { reopen: { runid: entry.runid } } });
|
|
1532
|
+
status(`The run ${entry.runid} reopened through the sessions seam.`);
|
|
1533
|
+
})().catch((error) => status(error instanceof Error ? error.message : String(error), true));
|
|
1534
|
+
});
|
|
1535
|
+
item.append(" ", reopen);
|
|
1536
|
+
}
|
|
1537
|
+
list.append(item);
|
|
1538
|
+
}
|
|
1539
|
+
recenttraynode.append(list);
|
|
1540
|
+
}
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
recenttraynode.textContent = error instanceof Error ? error.message : String(error);
|
|
1543
|
+
}
|
|
1544
|
+
try {
|
|
1545
|
+
const background = (await request({ kind: "ecosystem", background: { list: true } })) as {
|
|
1546
|
+
tray: Array<{ runid: string; title: string; outcome: string }>;
|
|
1547
|
+
};
|
|
1548
|
+
if (background.tray.length > 0) {
|
|
1549
|
+
const backgroundlist = document.createElement("ul");
|
|
1550
|
+
backgroundlist.className = "audit";
|
|
1551
|
+
const headline = document.createElement("li");
|
|
1552
|
+
headline.textContent = `Background runs: ${background.tray.length} finished entr${background.tray.length === 1 ? "y" : "ies"} from the background queue.`;
|
|
1553
|
+
backgroundlist.append(headline);
|
|
1554
|
+
for (const entry of background.tray.slice(0, 3))
|
|
1555
|
+
backgroundlist.append(
|
|
1556
|
+
Object.assign(document.createElement("li"), { textContent: `${entry.outcome} — ${entry.title}` }),
|
|
1557
|
+
);
|
|
1558
|
+
recenttraynode.append(backgroundlist);
|
|
1559
|
+
}
|
|
1560
|
+
} catch {
|
|
1561
|
+
/* a failing background tray read keeps the foreground tray rendered */
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/** Quickactions of the 1.1.65 family: only the actions the origin allowlist of the clicked tab permits surface in the popup. */
|
|
1566
|
+
const quickactionsnode = document.querySelector<HTMLElement>("#quickactions");
|
|
1567
|
+
async function renderquickactions(): Promise<void> {
|
|
1568
|
+
if (!quickactionsnode) return;
|
|
1569
|
+
try {
|
|
1570
|
+
const result = (await request({ kind: "views", quickaction: { list: {}, surface: true } })) as {
|
|
1571
|
+
actions?: Array<{ id: string; label: string; command: string }>;
|
|
1572
|
+
};
|
|
1573
|
+
const actions = result.actions ?? [];
|
|
1574
|
+
quickactionsnode.replaceChildren();
|
|
1575
|
+
if (actions.length === 0) {
|
|
1576
|
+
quickactionsnode.textContent =
|
|
1577
|
+
"No quickaction registers on this tab; grant its origin and the permitted actions surface.";
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
const list = document.createElement("ul");
|
|
1581
|
+
list.className = "audit";
|
|
1582
|
+
for (const action of actions) {
|
|
1583
|
+
const item = document.createElement("li");
|
|
1584
|
+
item.textContent = action.label;
|
|
1585
|
+
const run = document.createElement("button");
|
|
1586
|
+
run.type = "button";
|
|
1587
|
+
run.className = "secondary";
|
|
1588
|
+
run.textContent = "Run";
|
|
1589
|
+
run.addEventListener("click", () => {
|
|
1590
|
+
void (async () => {
|
|
1591
|
+
if (action.command === "opendashboardpage") {
|
|
1592
|
+
await chrome.tabs.create({ url: chrome.runtime.getURL("dashboardpage.html") });
|
|
1593
|
+
status("The dashboardpage opened in a new tab.");
|
|
1594
|
+
} else {
|
|
1595
|
+
await busaction("starttask");
|
|
1596
|
+
status(`The ${action.id} quickaction routed through the command bus.`);
|
|
1597
|
+
}
|
|
1598
|
+
})().catch((error) => status(error instanceof Error ? error.message : String(error), true));
|
|
1599
|
+
});
|
|
1600
|
+
item.append(" ", run);
|
|
1601
|
+
list.append(item);
|
|
1602
|
+
}
|
|
1603
|
+
quickactionsnode.append(list);
|
|
1604
|
+
} catch (error) {
|
|
1605
|
+
quickactionsnode.textContent = error instanceof Error ? error.message : String(error);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
void renderrecenttray();
|
|
1609
|
+
void renderquickactions();
|
|
1610
|
+
|
|
1611
|
+
/** Closedtabs of the 1.1.74 family: the popup lists the recently closed tab records under the user retention window and reopens one on request after the grant recheck. */
|
|
1612
|
+
const closedtabsnode = document.querySelector<HTMLElement>("#closedtabs");
|
|
1613
|
+
async function renderclosedtabs(): Promise<void> {
|
|
1614
|
+
if (!closedtabsnode) return;
|
|
1615
|
+
try {
|
|
1616
|
+
const view = (await request({ kind: "nav", view: true })) as {
|
|
1617
|
+
closedtabs?: Array<{ id: string; url: string; title: string; closedat: number; reopenedat?: number }>;
|
|
1618
|
+
};
|
|
1619
|
+
const records = view.closedtabs ?? [];
|
|
1620
|
+
closedtabsnode.replaceChildren();
|
|
1621
|
+
if (records.length === 0) {
|
|
1622
|
+
closedtabsnode.textContent = "No closed tab yet; the records stay under the user retention window.";
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
const list = document.createElement("ul");
|
|
1626
|
+
list.className = "audit";
|
|
1627
|
+
for (const record of records.slice(0, 6)) {
|
|
1628
|
+
const item = document.createElement("li");
|
|
1629
|
+
item.textContent = `${record.title || record.url}${record.reopenedat !== undefined ? " (already reopened)" : ""}`;
|
|
1630
|
+
if (record.reopenedat === undefined) {
|
|
1631
|
+
const reopen = document.createElement("button");
|
|
1632
|
+
reopen.type = "button";
|
|
1633
|
+
reopen.className = "secondary";
|
|
1634
|
+
reopen.textContent = "Reopen";
|
|
1635
|
+
reopen.addEventListener("click", () => {
|
|
1636
|
+
void (async () => {
|
|
1637
|
+
await request({ kind: "nav", reopen: { id: record.id } });
|
|
1638
|
+
status(`The closed tab ${record.url} reopened after the grant recheck.`);
|
|
1639
|
+
await renderclosedtabs();
|
|
1640
|
+
})().catch((error) => status(error instanceof Error ? error.message : String(error), true));
|
|
1641
|
+
});
|
|
1642
|
+
item.append(" ", reopen);
|
|
1643
|
+
}
|
|
1644
|
+
list.append(item);
|
|
1645
|
+
}
|
|
1646
|
+
closedtabsnode.append(list);
|
|
1647
|
+
} catch (error) {
|
|
1648
|
+
closedtabsnode.textContent = error instanceof Error ? error.message : String(error);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
void renderclosedtabs();
|