@wenathlan/devthink 2.0.15 → 2.0.19

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.
Files changed (444) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
  3. package/bridge.d.ts.map +1 -0
  4. package/bridge.js +1877 -0
  5. package/bun.d.ts.map +1 -0
  6. package/bun.js +52329 -0
  7. package/caps/background.json +712 -0
  8. package/caps/cli.json +360 -0
  9. package/caps/library.json +3611 -0
  10. package/caps/mcp.json +126 -0
  11. package/caps/pagebridge.json +377 -0
  12. package/caps/popup.json +51 -0
  13. package/caps/sidepanel.json +522 -0
  14. package/checksums.txt +142 -0
  15. package/cli.d.ts.map +1 -0
  16. package/{dist/cli.js → cli.js} +2 -2
  17. package/companion.d.ts.map +1 -0
  18. package/companion.js +389 -0
  19. package/crossbrowser.d.ts.map +1 -0
  20. package/crossbrowser.js +1191 -0
  21. package/dashdone.d.ts.map +1 -0
  22. package/dashdone.js +368 -0
  23. package/deno.d.ts.map +1 -0
  24. package/deno.js +52325 -0
  25. package/devthink.umd.js +52346 -0
  26. package/fixtures/bun.ts +7 -0
  27. package/fixtures/cjs.js +6 -0
  28. package/fixtures/deno.ts +6 -0
  29. package/fixtures/example-org-pagestate.json +22 -0
  30. package/fixtures/importers/automa-workflow.json +18 -0
  31. package/fixtures/importers/selenium-side.json +20 -0
  32. package/fixtures/importers/tabular-plan.csv +7 -0
  33. package/fixtures/importers/uivision-macro.json +10 -0
  34. package/fixtures/importers/v1-plan.json +13 -0
  35. package/fixtures/mcp-client.mjs +14 -0
  36. package/fixtures/pages/checkout.html +192 -0
  37. package/fixtures/pages/dashboard.html +110 -0
  38. package/fixtures/pages/feed.html +113 -0
  39. package/fixtures/pages/product-grid.html +150 -0
  40. package/fixtures/plans/form-inventory-plan.json +11 -0
  41. package/fixtures/plans/release-notes-plan.json +11 -0
  42. package/fixtures/recipes/agentscompete.json +11 -0
  43. package/fixtures/recipes/agentsforms.json +11 -0
  44. package/fixtures/recipes/agentsmonitor.json +11 -0
  45. package/fixtures/recipes/agentsreview.json +29 -0
  46. package/fixtures/recipes/agentswarm.json +11 -0
  47. package/fixtures/recipes/fillcalendar.json +12 -0
  48. package/fixtures/recipes/fillcheckout.json +13 -0
  49. package/fixtures/recipes/filldependentdropdowns.json +12 -0
  50. package/fixtures/recipes/fillfromcsv.json +13 -0
  51. package/fixtures/recipes/fillloginform.json +12 -0
  52. package/fixtures/recipes/fillmultistepwizard.json +13 -0
  53. package/fixtures/recipes/fillregistration.json +12 -0
  54. package/fixtures/recipes/fillsearchfilters.json +12 -0
  55. package/fixtures/recipes/filltypeahead.json +11 -0
  56. package/fixtures/recipes/monitoralerts.json +12 -0
  57. package/fixtures/recipes/monitoravailability.json +11 -0
  58. package/fixtures/recipes/monitornetworkquiet.json +12 -0
  59. package/fixtures/recipes/monitorpagechanges.json +11 -0
  60. package/fixtures/recipes/monitorpricechange.json +11 -0
  61. package/fixtures/recipes/monitorschedule.json +12 -0
  62. package/fixtures/recipes/scrapedynamicprices.json +13 -0
  63. package/fixtures/recipes/scrapeimagegallery.json +12 -0
  64. package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
  65. package/fixtures/recipes/scrapenestedlists.json +12 -0
  66. package/fixtures/recipes/scrapeproductgrid.json +12 -0
  67. package/fixtures/recipes/scrapereviews.json +12 -0
  68. package/fixtures/recipes/scrapesearchresults.json +12 -0
  69. package/fixtures/recipes/scrapetablepagination.json +12 -0
  70. package/fixtures/recipes/testaccessibility.json +34 -0
  71. package/fixtures/recipes/testconsoleerrors.json +12 -0
  72. package/fixtures/recipes/testdeeplinks.json +12 -0
  73. package/fixtures/recipes/testformvalidation.json +13 -0
  74. package/fixtures/recipes/testlayoutshift.json +12 -0
  75. package/fixtures/recipes/testlinkcheck.json +12 -0
  76. package/fixtures/recipes/testloadtiming.json +12 -0
  77. package/fixtures/recipes/testvisualdiff.json +12 -0
  78. package/fixtures/workflows/changelog-digest-workflow.json +13 -0
  79. package/fixtures/workflows/form-survey-workflow.json +13 -0
  80. package/gallery.json +706 -0
  81. package/hardening.d.ts.map +1 -0
  82. package/hardening.js +188 -0
  83. package/headless.d.ts.map +1 -0
  84. package/headless.js +11522 -0
  85. package/http.d.ts.map +1 -0
  86. package/http.js +2165 -0
  87. package/index.cjs +54682 -0
  88. package/index.d.ts.map +1 -0
  89. package/index.js +52310 -0
  90. package/index.neutral.js +46343 -0
  91. package/{web/manifest.json → manifest.json} +1 -1
  92. package/mcp.d.ts.map +1 -0
  93. package/mcp.js +20515 -0
  94. package/memory.d.ts.map +1 -0
  95. package/{memory.ts → memory.js} +2310 -3834
  96. package/nativehost.template.json +10 -0
  97. package/node.cjs +54686 -0
  98. package/node.d.ts.map +1 -0
  99. package/pack.d.ts.map +1 -0
  100. package/pack.js +805 -0
  101. package/package.json +83 -176
  102. package/policy.d.ts.map +1 -0
  103. package/{policy.ts → policy.js} +4679 -5073
  104. package/progress.d.ts.map +1 -0
  105. package/progress.js +645 -0
  106. package/protocol.d.ts.map +1 -0
  107. package/protocol.js +8334 -0
  108. package/schemas/audit.schema.json +226 -0
  109. package/schemas/capability.schema.json +113 -0
  110. package/schemas/envelope.schema.json +162 -0
  111. package/schemas/memory.schema.json +51 -0
  112. package/schemas/observation.schema.json +113 -0
  113. package/schemas/plan.schema.json +208 -0
  114. package/schemas/progress.schema.json +126 -0
  115. package/schemas/proposal.schema.json +68 -0
  116. package/schemas/session.schema.json +82 -0
  117. package/schemas/tool.schema.json +94 -0
  118. package/server.d.ts.map +1 -0
  119. package/server.js +28877 -0
  120. package/umd-example.html +18 -0
  121. package/umd.d.ts.map +1 -0
  122. package/accounts.ts +0 -2087
  123. package/agent.ts +0 -2331
  124. package/antigravity.ts +0 -2139
  125. package/apifreeze.ts +0 -4146
  126. package/auth.ts +0 -642
  127. package/background.ts +0 -16837
  128. package/bridge.ts +0 -2080
  129. package/build.ts +0 -54
  130. package/bun.ts +0 -26
  131. package/capture.ts +0 -1614
  132. package/checksecrets.ts +0 -72
  133. package/cjs.ts +0 -9
  134. package/cli.ts +0 -3500
  135. package/commands.ts +0 -1101
  136. package/companion.ts +0 -450
  137. package/compatibility.ts +0 -37
  138. package/config.ts +0 -3137
  139. package/constants.ts +0 -943
  140. package/core.ts +0 -1473
  141. package/crossbrowser.ts +0 -1359
  142. package/dashdone.ts +0 -605
  143. package/data.ts +0 -943
  144. package/database.ts +0 -133
  145. package/debug.ts +0 -850
  146. package/deno.ts +0 -27
  147. package/devthink.ts +0 -707
  148. package/dist/accounts.d.ts +0 -409
  149. package/dist/agent.d.ts +0 -837
  150. package/dist/antigravity.d.ts +0 -380
  151. package/dist/apifreeze.d.ts +0 -113
  152. package/dist/auth.d.ts +0 -224
  153. package/dist/capture.d.ts +0 -595
  154. package/dist/cjs.d.ts +0 -6
  155. package/dist/commands.d.ts +0 -310
  156. package/dist/compatibility.d.ts +0 -17
  157. package/dist/config.d.ts +0 -1853
  158. package/dist/constants.d.ts +0 -637
  159. package/dist/core.d.ts +0 -361
  160. package/dist/data.d.ts +0 -330
  161. package/dist/database.d.ts +0 -24
  162. package/dist/debug.d.ts +0 -270
  163. package/dist/engine.d.ts +0 -74
  164. package/dist/environments.d.ts +0 -244
  165. package/dist/evidence.d.ts +0 -277
  166. package/dist/export.d.ts +0 -311
  167. package/dist/fingerprint.d.ts +0 -594
  168. package/dist/flow.d.ts +0 -300
  169. package/dist/gates.d.ts +0 -103
  170. package/dist/identity.d.ts +0 -51
  171. package/dist/ids.d.ts +0 -7
  172. package/dist/llm.d.ts +0 -652
  173. package/dist/models.d.ts +0 -1807
  174. package/dist/net.d.ts +0 -325
  175. package/dist/neutral.d.ts +0 -41
  176. package/dist/oauth.d.ts +0 -703
  177. package/dist/page.d.ts +0 -1166
  178. package/dist/perf.d.ts +0 -481
  179. package/dist/plan.d.ts +0 -134
  180. package/dist/project.d.ts +0 -395
  181. package/dist/providers.d.ts +0 -37
  182. package/dist/quota.d.ts +0 -453
  183. package/dist/recovery.d.ts +0 -621
  184. package/dist/request.d.ts +0 -1492
  185. package/dist/run.d.ts +0 -779
  186. package/dist/runtime.d.ts +0 -212
  187. package/dist/search.d.ts +0 -375
  188. package/dist/security.d.ts +0 -534
  189. package/dist/serve.d.ts +0 -136
  190. package/dist/session.d.ts +0 -296
  191. package/dist/storage.d.ts +0 -53
  192. package/dist/streaming.d.ts +0 -1119
  193. package/dist/swarm.d.ts +0 -328
  194. package/dist/tools.d.ts +0 -170
  195. package/dist/types.d.ts +0 -8357
  196. package/dist/utils.d.ts +0 -82
  197. package/dist/version.d.ts +0 -7
  198. package/dist/versionregistry.d.ts +0 -406
  199. package/dist/views.d.ts +0 -579
  200. package/dist/workbenchsession.d.ts +0 -69
  201. package/dist/workflow.d.ts +0 -649
  202. package/docs/readme.md +0 -92
  203. package/docs/schemas/antigravity.json +0 -192
  204. package/engine.ts +0 -1524
  205. package/environments.ts +0 -1005
  206. package/evidence.ts +0 -643
  207. package/export.ts +0 -889
  208. package/fingerprint.ts +0 -1458
  209. package/flow.ts +0 -1190
  210. package/gates.ts +0 -307
  211. package/gateway.ts +0 -34
  212. package/hardening.ts +0 -191
  213. package/headless.ts +0 -369
  214. package/http.ts +0 -2110
  215. package/identity.ts +0 -246
  216. package/ids.ts +0 -26
  217. package/index.ts +0 -369
  218. package/llm.ts +0 -2682
  219. package/mcp.ts +0 -1835
  220. package/models.ts +0 -3805
  221. package/modes.ts +0 -43
  222. package/net.ts +0 -1214
  223. package/neutral.ts +0 -362
  224. package/node.ts +0 -26
  225. package/oauth.ts +0 -2216
  226. package/offscreen.ts +0 -170
  227. package/pack.ts +0 -956
  228. package/page.ts +0 -7824
  229. package/pagebridge.ts +0 -1116
  230. package/perf.ts +0 -957
  231. package/plan.ts +0 -672
  232. package/plugin.ts +0 -76
  233. package/prisma.config.ts +0 -27
  234. package/progress.ts +0 -1308
  235. package/project.ts +0 -1291
  236. package/protocol.ts +0 -4187
  237. package/providers.ts +0 -246
  238. package/quota.ts +0 -1887
  239. package/recovery.ts +0 -2433
  240. package/request.ts +0 -4482
  241. package/run.ts +0 -2135
  242. package/runtime.ts +0 -526
  243. package/search.ts +0 -1760
  244. package/security.ts +0 -1673
  245. package/serve.ts +0 -419
  246. package/server.ts +0 -5538
  247. package/session.ts +0 -1162
  248. package/storage.ts +0 -182
  249. package/streaming.ts +0 -4101
  250. package/swarm.ts +0 -884
  251. package/sync.ts +0 -56
  252. package/tests/agentcert.test.ts +0 -128
  253. package/tests/agentfleet.test.ts +0 -214
  254. package/tests/agentreview.test.ts +0 -241
  255. package/tests/agentstream.test.ts +0 -335
  256. package/tests/agentwork.test.ts +0 -356
  257. package/tests/apifreeze.test.ts +0 -290
  258. package/tests/apimap.test.ts +0 -112
  259. package/tests/approvalgate.test.ts +0 -175
  260. package/tests/artifactmanifest.test.ts +0 -204
  261. package/tests/attentionfeed.test.ts +0 -163
  262. package/tests/backgroundruns.test.ts +0 -100
  263. package/tests/batchscheduling.test.ts +0 -131
  264. package/tests/blackboard.test.ts +0 -238
  265. package/tests/bridge.test.ts +0 -1088
  266. package/tests/browsercoverage.test.ts +0 -134
  267. package/tests/browserpolyfills.test.ts +0 -124
  268. package/tests/build.mjs +0 -1260
  269. package/tests/clientauth.test.ts +0 -223
  270. package/tests/clitools.test.ts +0 -578
  271. package/tests/confirmgates.test.ts +0 -190
  272. package/tests/containerpack.test.ts +0 -143
  273. package/tests/controlflow.test.ts +0 -2120
  274. package/tests/coordination.test.ts +0 -345
  275. package/tests/copyscreen.test.ts +0 -139
  276. package/tests/core.test.ts +0 -417
  277. package/tests/costcert.test.ts +0 -95
  278. package/tests/crossbrowser.test.ts +0 -115
  279. package/tests/cspaudit.test.ts +0 -79
  280. package/tests/dashdone.test.ts +0 -615
  281. package/tests/datagrid.test.ts +0 -137
  282. package/tests/doccheck.test.ts +0 -107
  283. package/tests/dombatch.test.ts +0 -109
  284. package/tests/emulation.test.ts +0 -282
  285. package/tests/environments.test.ts +0 -259
  286. package/tests/evidenceviews.test.ts +0 -88
  287. package/tests/exporttools.test.ts +0 -185
  288. package/tests/firefoxprep.test.ts +0 -134
  289. package/tests/flowlibrary.test.ts +0 -242
  290. package/tests/flowrun.test.ts +0 -303
  291. package/tests/forensics.test.ts +0 -439
  292. package/tests/gateway.test.ts +0 -939
  293. package/tests/headlesslib.test.ts +0 -171
  294. package/tests/httpstream.test.ts +0 -277
  295. package/tests/immutablelog.test.ts +0 -157
  296. package/tests/inboundguard.test.ts +0 -177
  297. package/tests/lazyload.test.ts +0 -72
  298. package/tests/librarymodes.test.ts +0 -623
  299. package/tests/llm.test.ts +0 -696
  300. package/tests/manifest.test.ts +0 -226
  301. package/tests/maskinputs.test.ts +0 -150
  302. package/tests/matrixverify.test.ts +0 -111
  303. package/tests/mavenpack.test.ts +0 -75
  304. package/tests/mcpserver.test.ts +0 -704
  305. package/tests/memory.test.ts +0 -4155
  306. package/tests/memorycare.test.ts +0 -230
  307. package/tests/migrateplan.test.ts +0 -349
  308. package/tests/minimization.test.ts +0 -414
  309. package/tests/modelroute.test.ts +0 -112
  310. package/tests/multiagent.test.ts +0 -440
  311. package/tests/nativebridge.test.ts +0 -807
  312. package/tests/navigation.test.ts +0 -284
  313. package/tests/nugetpack.test.ts +0 -83
  314. package/tests/orchestration.test.ts +0 -449
  315. package/tests/originpolicy.test.ts +0 -405
  316. package/tests/outputcompare.test.ts +0 -97
  317. package/tests/package.test.ts +0 -91
  318. package/tests/pagebridge.test.ts +0 -5909
  319. package/tests/pentest.test.ts +0 -75
  320. package/tests/perfrecords.test.ts +0 -257
  321. package/tests/permdiff.test.ts +0 -85
  322. package/tests/phishguard.test.ts +0 -128
  323. package/tests/pickerviews.test.ts +0 -155
  324. package/tests/pipelines.test.ts +0 -381
  325. package/tests/planlint.test.ts +0 -240
  326. package/tests/planreview.test.ts +0 -383
  327. package/tests/platformtargets.test.ts +0 -56
  328. package/tests/policy.test.ts +0 -8284
  329. package/tests/portability.test.ts +0 -96
  330. package/tests/progress.test.ts +0 -1036
  331. package/tests/promptlibrary.test.ts +0 -116
  332. package/tests/protocol.test.ts +0 -4793
  333. package/tests/publishingpipeline.test.ts +0 -314
  334. package/tests/quickactions.test.ts +0 -164
  335. package/tests/readiness.test.ts +0 -78
  336. package/tests/recipes.test.ts +0 -163
  337. package/tests/redactshots.test.ts +0 -216
  338. package/tests/registernvidiakeys.ts +0 -51
  339. package/tests/relayserve.test.ts +0 -304
  340. package/tests/releasenotes.test.ts +0 -276
  341. package/tests/resilience.test.ts +0 -467
  342. package/tests/resourceaware.test.ts +0 -187
  343. package/tests/runbudget.test.ts +0 -125
  344. package/tests/runhistory.test.ts +0 -299
  345. package/tests/runreplay.test.ts +0 -113
  346. package/tests/runresume.test.ts +0 -184
  347. package/tests/runstate.test.ts +0 -288
  348. package/tests/runtimeadapters.test.ts +0 -251
  349. package/tests/safariskeleton.test.ts +0 -115
  350. package/tests/sandboxframe.test.ts +0 -196
  351. package/tests/sbom.test.ts +0 -105
  352. package/tests/secretvault.test.ts +0 -188
  353. package/tests/selcache.test.ts +0 -111
  354. package/tests/sessioninterface.test.ts +0 -791
  355. package/tests/sessions.test.ts +0 -591
  356. package/tests/siteprefs.test.ts +0 -228
  357. package/tests/snapshotdelta.test.ts +0 -165
  358. package/tests/statusviews.test.ts +0 -237
  359. package/tests/stepmeter.test.ts +0 -200
  360. package/tests/streamparse.test.ts +0 -163
  361. package/tests/stylepruning.test.ts +0 -90
  362. package/tests/surfaces.test.ts +0 -282
  363. package/tests/sweep.test.ts +0 -78
  364. package/tests/syncbridge.test.ts +0 -164
  365. package/tests/taskqueue.test.ts +0 -206
  366. package/tests/toolcalls.test.ts +0 -336
  367. package/tests/toolcatalog.test.ts +0 -201
  368. package/tests/torture-agent.test.ts +0 -706
  369. package/tests/torture-auth.test.ts +0 -461
  370. package/tests/torture-commands.test.ts +0 -895
  371. package/tests/torture-data.test.ts +0 -923
  372. package/tests/torture-environments.test.ts +0 -285
  373. package/tests/torture-export.test.ts +0 -263
  374. package/tests/torture-http.test.ts +0 -1379
  375. package/tests/torture-llm.test.ts +0 -808
  376. package/tests/torture-memory.test.ts +0 -1084
  377. package/tests/torture-plan.test.ts +0 -829
  378. package/tests/torture-run.test.ts +0 -626
  379. package/tests/torture-session.test.ts +0 -928
  380. package/tests/torture-swarm.test.ts +0 -1245
  381. package/tests/torture-tools.test.ts +0 -964
  382. package/tests/torture-workflow.test.ts +0 -2354
  383. package/tests/tourviews.test.ts +0 -52
  384. package/tests/transparency.test.ts +0 -147
  385. package/tests/trigger.test.ts +0 -941
  386. package/tests/v1sunset.test.ts +0 -428
  387. package/tests/virtlist.test.ts +0 -94
  388. package/tests/vision.test.ts +0 -540
  389. package/tests/vsixpack.test.ts +0 -145
  390. package/tests/webapi.test.ts +0 -523
  391. package/tests/workflow.test.ts +0 -1785
  392. package/tests/workfloweditor.test.ts +0 -759
  393. package/tests/xpipack.test.ts +0 -128
  394. package/tools.ts +0 -734
  395. package/types.ts +0 -9908
  396. package/umd.ts +0 -9
  397. package/utils.ts +0 -249
  398. package/version.ts +0 -8
  399. package/versionregistry.ts +0 -1310
  400. package/views.ts +0 -2153
  401. package/vitest.config.ts +0 -12
  402. package/web/App.tsx +0 -78
  403. package/web/README.md +0 -47
  404. package/web/capacitor.config.ts +0 -46
  405. package/web/const.ts +0 -19
  406. package/web/control.shell.tsx +0 -66
  407. package/web/dashboardpage.ts +0 -554
  408. package/web/db.ts +0 -493
  409. package/web/design.html +0 -541
  410. package/web/gatewayview/Gateway.tsx +0 -345
  411. package/web/gatewayview/config.ts +0 -1042
  412. package/web/gatewayview/definition.ts +0 -174
  413. package/web/icons.ts +0 -15
  414. package/web/optionspage.ts +0 -691
  415. package/web/popup.ts +0 -1651
  416. package/web/schema.prisma +0 -176
  417. package/web/sidepanel.ts +0 -7941
  418. package/web/sitemanifest.json +0 -13
  419. package/web/terminal-ui.tsx +0 -622
  420. package/web/transparencypage.ts +0 -541
  421. package/web/vite.config.ts +0 -39
  422. package/web/workspace.ts +0 -23
  423. package/workbenchmemory.ts +0 -104
  424. package/workbenchsession.ts +0 -304
  425. package/workflow.ts +0 -4495
  426. /package/{dist/bun.d.ts → bun.d.ts} +0 -0
  427. /package/{dist/cli.d.ts → cli.d.ts} +0 -0
  428. /package/{dist/companion.d.ts → companion.d.ts} +0 -0
  429. /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
  430. /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
  431. /package/{dist/deno.d.ts → deno.d.ts} +0 -0
  432. /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
  433. /package/{dist/headless.d.ts → headless.d.ts} +0 -0
  434. /package/{dist/http.d.ts → http.d.ts} +0 -0
  435. /package/{dist/index.d.ts → index.d.ts} +0 -0
  436. /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
  437. /package/{dist/memory.d.ts → memory.d.ts} +0 -0
  438. /package/{dist/node.d.ts → node.d.ts} +0 -0
  439. /package/{dist/pack.d.ts → pack.d.ts} +0 -0
  440. /package/{dist/policy.d.ts → policy.d.ts} +0 -0
  441. /package/{dist/progress.d.ts → progress.d.ts} +0 -0
  442. /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
  443. /package/{dist/server.d.ts → server.d.ts} +0 -0
  444. /package/{dist/umd.d.ts → umd.d.ts} +0 -0
package/views.ts DELETED
@@ -1,2153 +0,0 @@
1
- /**
2
- * The views module of the 1.1.90 consolidation: every correlated variation of the interface surface logic interned in this one file, so the module family carries one surface without duplicate variations.
3
- * The correlation is the interface surface family itself: surfaces holds the commandpalette, the taskinput proposals, the onboarding walkthrough and the command bus with the broadcast channel every surface shares; statusviews carries the statusbadge, the done and attention notifications, the recent tray and the stetoast stack; pickerviews scores the element candidates of a picker session with its target halo, page chips and guided tips; tourviews replays the featuretour and localizes the a11ylabels through the locale bundles; evidenceviews renders the shotpanel and the compareviewer pair of the capture evidence; datagrid infers the column types of extraction results with its sorting, filtering, range selection and masked exports; virtlist windows the long lists every surface renders; siteprefs resolves the site profiles, the theme tokens, the locale bundles and the shortcut bindings; quickactions carries the quickaction catalog with its omnibox task parsing and shortcut grammar; and attentionfeed collects, dedupes, ranks and dismisses the attention entries every surface surfaces.
4
- * No palette depth, list window, locale set or shortcut is ever hardcoded: every bound stays the user's choice with no engine cap, and no surface action ever bypasses the human review.
5
- */
6
-
7
- /* ── Merged from surfaces.ts ── */
8
- import type {
9
- broadcastchannelkind,
10
- broadcastframe,
11
- onboardingstate,
12
- onboardingstep,
13
- paletteentry,
14
- palettematch,
15
- paletteuserecord,
16
- surfaceaction,
17
- surfaceroute,
18
- taskinputsubmission,
19
- uisurface,
20
- notificationpayload,
21
- recenttrayentry,
22
- statusbadgestate,
23
- stetoast,
24
- guidedtip,
25
- pagechipconfirmation,
26
- pickercandidate,
27
- pickersession,
28
- targethalo,
29
- a11ylabel,
30
- featuretourstop,
31
- localebundle,
32
- compareviewerpair,
33
- shotpanelview,
34
- datagridcolumn,
35
- datagridrow,
36
- datagridview,
37
- exportmenudescriptor,
38
- virtlistwindow,
39
- darklighttokens,
40
- siteprofile,
41
- shortcutbinding,
42
- omniboxtasksubmission,
43
- quickaction,
44
- attentionentry,
45
- attentioncause,
46
- } from "./types.js";
47
- import { paletteactiongate, planreviewgate, taskinputproposalgate } from "./policy.js";
48
- import { randomid } from "./memory.js";
49
-
50
- /**
51
- * Interface surface logic of the 1.1.64 family, part one.
52
- * The popup, the sidepanel, the dashboardpage, the optionspage and the onboarding share this pure logic: the commandpalette registers its catalog from every module at startup, lists only the actions the current capability set allows and ranks fuzzy matches over ids, labels and keywords with the recent commands first; the taskinput builds submissions with the active origin and the page outline attached and routes them through the same proposal flow as the api; the onboarding walks its four steps once on first install, replays on demand and writes a single consent scoped event on completion; the command bus routes every surface action through the same policy gates; and the single broadcast channel frames every run state, logstream, session store and settings change the surfaces subscribe to.
53
- * No window, limit or ranking depth is ever hardcoded: every bound stays the user's choice with no engine cap, and no surface action ever bypasses the human review.
54
- */
55
-
56
- /** The commandpalette catalog registered from every module at startup: each entry routes through the command bus with the permission and session needs it declares. */
57
- export function surfacepalette(): paletteentry[] {
58
- return [
59
- {
60
- id: "starttask",
61
- label: "Start task",
62
- keywords: ["task", "objective", "run", "goal", "plan"],
63
- action: { command: "starttask", surface: "popup" },
64
- },
65
- {
66
- id: "pauserun",
67
- label: "Pause run",
68
- keywords: ["pause", "hold", "stop", "run"],
69
- action: { command: "pauserun", surface: "popup", session: true },
70
- },
71
- {
72
- id: "resumerun",
73
- label: "Resume run",
74
- keywords: ["resume", "continue", "unpause", "run"],
75
- action: { command: "resumerun", surface: "popup", session: true },
76
- },
77
- {
78
- id: "cancelrun",
79
- label: "Cancel run",
80
- keywords: ["cancel", "stop", "rollback", "queued"],
81
- action: { command: "cancelrun", surface: "popup", session: true },
82
- },
83
- {
84
- id: "resumesession",
85
- label: "Resume session",
86
- keywords: ["session", "resume", "grid", "reopen"],
87
- action: { command: "resumesession", surface: "sidepanel" },
88
- },
89
- {
90
- id: "stepapprove",
91
- label: "Review step",
92
- keywords: ["approve", "reject", "edit", "step", "review", "plancard"],
93
- action: { command: "stepapprove", surface: "sidepanel", session: true },
94
- },
95
- {
96
- id: "diffpreview",
97
- label: "Preview step diff",
98
- keywords: ["diff", "preview", "before", "after", "write"],
99
- action: { command: "diffpreview", surface: "sidepanel", session: true },
100
- },
101
- {
102
- id: "historysearch",
103
- label: "Search history",
104
- keywords: ["history", "search", "notes", "summaries", "corpus"],
105
- action: { command: "historysearch", surface: "dashboardpage" },
106
- },
107
- {
108
- id: "revokeconsent",
109
- label: "Revoke consent",
110
- keywords: ["revoke", "consent", "allowlist", "origin", "grant"],
111
- action: { command: "revokeconsent", surface: "dashboardpage", session: true },
112
- },
113
- {
114
- id: "opentransparencypage",
115
- label: "Open transparency page",
116
- keywords: ["transparency", "grants", "permissions", "diff"],
117
- action: { command: "opentransparencypage", surface: "optionspage" },
118
- },
119
- {
120
- id: "opendashboardpage",
121
- label: "Open dashboard",
122
- keywords: ["dashboard", "sessions", "runs", "notes", "full"],
123
- action: { command: "opendashboardpage", surface: "dashboardpage" },
124
- },
125
- {
126
- id: "openoptionspage",
127
- label: "Open options",
128
- keywords: ["options", "settings", "preferences", "configure"],
129
- action: { command: "openoptionspage", surface: "optionspage" },
130
- },
131
- {
132
- id: "copyauditexcerpt",
133
- label: "Copy audit excerpt",
134
- keywords: ["audit", "excerpt", "copy", "verified", "range"],
135
- action: { command: "copyauditexcerpt", surface: "dashboardpage" },
136
- },
137
- {
138
- id: "replayonboarding",
139
- label: "Replay onboarding",
140
- keywords: ["onboarding", "tour", "walkthrough", "replay", "first"],
141
- action: { command: "replayonboarding", surface: "onboarding" },
142
- },
143
- ];
144
- }
145
-
146
- /** Lists only the palette actions the current capability set and session state allow: every command rides its existing permission gate before it lists. */
147
- export function palettecommandsof(
148
- entries: paletteentry[],
149
- input: { granted: string[]; sessionactive: boolean },
150
- ): paletteentry[] {
151
- return entries.filter(
152
- (entry) =>
153
- paletteactiongate({ action: entry.action, granted: input.granted, sessionactive: input.sessionactive }).allowed,
154
- );
155
- }
156
-
157
- /** Scores one fuzzy query against an entry id, label and keywords: exact id matches rank highest, label and keyword hits rank next and each matched keyword lifts the score. */
158
- function fuzzyentryscore(entry: paletteentry, query: string): number {
159
- const text = query.trim().toLowerCase();
160
- if (text === "") return 1;
161
- const id = entry.id.toLowerCase();
162
- const label = entry.label.toLowerCase();
163
- if (id === text || label === text) return 100;
164
- let score = 0;
165
- if (id.includes(text)) score += 40;
166
- if (label.includes(text)) score += 30;
167
- for (const keyword of entry.keywords) {
168
- const lower = keyword.toLowerCase();
169
- if (lower === text) score += 20;
170
- else if (lower.includes(text)) score += 10;
171
- }
172
- if (score === 0 && text.length > 1) {
173
- // subsequence fuzzy match: every query letter appears in order inside the label or the id
174
- for (const haystack of [label, id]) {
175
- let cursor = 0;
176
- let matched = true;
177
- for (const letter of text) {
178
- const found = haystack.indexOf(letter, cursor);
179
- if (found === -1) {
180
- matched = false;
181
- break;
182
- }
183
- cursor = found + 1;
184
- }
185
- if (matched) {
186
- score += 15;
187
- break;
188
- }
189
- }
190
- }
191
- return score;
192
- }
193
-
194
- /** Ranks the fuzzy palette matches for one query: ids, labels and keywords match while the most recently used commands rank first among equal scores. */
195
- export function palettequery(
196
- entries: paletteentry[],
197
- input: { text: string; usage: paletteuserecord[]; recentwindow?: number },
198
- ): palettematch[] {
199
- const text = input.text.trim();
200
- const matches = entries
201
- .map((entry) => ({ entry, score: fuzzyentryscore(entry, text) }))
202
- .filter((match) => match.score > 0);
203
- const lastusedof = (command: string): number =>
204
- input.usage.find((record) => record.command === command)?.lastusedat ?? 0;
205
- const countof = (command: string): number => input.usage.find((record) => record.command === command)?.count ?? 0;
206
- const recentwindow = input.recentwindow;
207
- const ranked = matches.sort((left, right) => {
208
- if (right.score !== left.score) return right.score - left.score;
209
- const leftrecent =
210
- recentwindow === undefined
211
- ? 0
212
- : countof(left.entry.action.command) > 0 &&
213
- lastusedof(left.entry.action.command) >= lastusedof(right.entry.action.command)
214
- ? 1
215
- : 0;
216
- const rightrecent =
217
- recentwindow === undefined
218
- ? 0
219
- : countof(right.entry.action.command) > 0 &&
220
- lastusedof(right.entry.action.command) >= lastusedof(left.entry.action.command)
221
- ? 1
222
- : 0;
223
- if (rightrecent !== leftrecent) return rightrecent - leftrecent;
224
- return lastusedof(right.entry.action.command) - lastusedof(left.entry.action.command);
225
- });
226
- return ranked.map((match) => ({
227
- entry: match.entry,
228
- score: match.score,
229
- reason:
230
- match.score >= 100
231
- ? `The query matches the ${match.entry.id} command exactly.`
232
- : `The query matches the label or the keywords of the ${match.entry.id} command${countof(match.entry.action.command) > 0 ? ` and its ${countof(match.entry.action.command)} recorded use${countof(match.entry.action.command) === 1 ? "" : "s"} rank it first among equals` : ""}.`,
233
- }));
234
- }
235
-
236
- /** Records one commandpalette use: the count grows and the last use time moves so the recent first ranking reads both. */
237
- export function paletteuseafter(usage: paletteuserecord[], command: string, now: number): paletteuserecord[] {
238
- const existing = usage.find((record) => record.command === command);
239
- if (existing === undefined) return [{ command, count: 1, lastusedat: now }, ...usage];
240
- return usage.map((record) =>
241
- record.command === command ? { ...record, count: record.count + 1, lastusedat: now } : record,
242
- );
243
- }
244
-
245
- /** Builds one taskinput submission with the active origin and the page outline context attached; an empty text or origin refuses the submission. */
246
- export function taskinputof(input: {
247
- text: string;
248
- context?: string;
249
- origin: string;
250
- surface: uisurface;
251
- at: number;
252
- }): taskinputsubmission {
253
- if (input.text.trim() === "") throw new Error("The taskinput needs its natural language goal.");
254
- if (input.origin.trim() === "") throw new Error("The taskinput needs its active origin scope.");
255
- return {
256
- id: randomid(),
257
- text: input.text.trim(),
258
- context: input.context ?? "",
259
- origin: input.origin.trim(),
260
- surface: input.surface,
261
- at: input.at,
262
- };
263
- }
264
-
265
- /** Keeps the taskinput history per profile with the newest entry first; an absent retention window keeps every entry. */
266
- export function taskhistoryafter(
267
- history: taskinputsubmission[],
268
- entry: taskinputsubmission,
269
- retention: number | undefined,
270
- now: number,
271
- ): taskinputsubmission[] {
272
- if (retention === undefined) return [entry, ...history];
273
- return [entry, ...history].filter((candidate) => now - candidate.at < retention);
274
- }
275
-
276
- /** The five onboarding steps: the origin grants, the plan review, the run control, the log audit walkthrough of a first run and the optional library walkthrough stop of the 1.1.66 ecosystem family. */
277
- export function onboardingsteps(): onboardingstep[] {
278
- return [
279
- {
280
- id: "origingrants",
281
- surface: "popup",
282
- title: "Origin grants",
283
- body: "Devthink denies automation by default; grant one exact origin at a time from the popup and every run stays inside the granted origins.",
284
- completion: "origingrantscompleted",
285
- },
286
- {
287
- id: "planreview",
288
- surface: "sidepanel",
289
- title: "Plan review",
290
- body: "Every task becomes a plan of reviewed steps; read the plancards of each risk class and approve, reject or edit one step at a time.",
291
- completion: "planreviewcompleted",
292
- },
293
- {
294
- id: "runcontrol",
295
- surface: "sidepanel",
296
- title: "Run control",
297
- body: "Runs start, pause, resume and cancel under your hand; a cancelled run rolls only its queued steps back while the executed steps stay sealed.",
298
- completion: "runcontrolcompleted",
299
- },
300
- {
301
- id: "logaudit",
302
- surface: "dashboardpage",
303
- title: "Log audit",
304
- body: "The immutable log chains every step transition with masked values; open the dashboard, verify the chain and copy a verified range as an audit excerpt.",
305
- completion: "logauditcompleted",
306
- },
307
- {
308
- id: "library",
309
- surface: "dashboardpage",
310
- title: "Flow library",
311
- body: "The flowlibrary shares reviewed workflow templates: browse an entry, read its step list and grant diff, and every install still lands as a proposal behind the same review.",
312
- completion: "librarycompleted",
313
- optional: true,
314
- },
315
- {
316
- id: "performance",
317
- surface: "optionspage",
318
- title: "Performance",
319
- body: "Heavy modules load lazily, snapshots compute deltas against their base and big tables extract in resumable chunks; the optionspage shows the startup budget and every window stays your choice.",
320
- completion: "performancecompleted",
321
- optional: true,
322
- },
323
- ];
324
- }
325
-
326
- /** Starts the onboarding once on first install or on an explicit replay: a fresh walkthrough begins while an already done state resets. */
327
- export function onboardingstart(previous: onboardingstate | undefined, now: number): onboardingstate {
328
- return { stepscompleted: [], done: false, startedat: now };
329
- }
330
-
331
- /** Completes one onboarding step: the walkthrough records the step and a full completion of every mandatory stop writes exactly one consent scoped event while the optional library stop enriches without blocking. */
332
- export function onboardingcomplete(
333
- state: onboardingstate,
334
- stepid: string,
335
- now: number,
336
- ): { state: onboardingstate; consentevent?: string } {
337
- const steps = onboardingsteps();
338
- const step = steps.find((candidate) => candidate.id === stepid);
339
- if (step === undefined) throw new Error(`The onboarding knows no ${stepid} step.`);
340
- const completed = state.stepscompleted.includes(stepid) ? state.stepscompleted : [...state.stepscompleted, stepid];
341
- const done = steps
342
- .filter((candidate) => candidate.optional !== true)
343
- .every((candidate) => completed.includes(candidate.id));
344
- if (!done) return { state: { ...state, stepscompleted: completed, done: false } };
345
- const consentevent = "onboardingconsentgranted";
346
- return {
347
- state: {
348
- stepscompleted: completed,
349
- done: true,
350
- ...(state.startedat !== undefined ? { startedat: state.startedat } : {}),
351
- consentevent,
352
- completedat: now,
353
- },
354
- consentevent,
355
- };
356
- }
357
-
358
- /** Builds one broadcast frame of the single surface channel: every run state, logstream, session store and settings change rides the same channel. */
359
- export function broadcastframeof(input: {
360
- channel: broadcastchannelkind;
361
- surface: uisurface | "background";
362
- summary: string;
363
- at: number;
364
- }): broadcastframe {
365
- if (input.summary.trim() === "") throw new Error("The broadcast frame needs its summary.");
366
- return { channel: input.channel, surface: input.surface, summary: input.summary, at: input.at };
367
- }
368
-
369
- /** Maps one audit kind onto the broadcast channel its frame rides: run control kinds carry run state, session store kinds carry session updates and the rest streams as logstream frames. */
370
- export function broadcastchannelof(kind: string): broadcastchannelkind {
371
- if (
372
- [
373
- "session",
374
- "proposal",
375
- "approval",
376
- "action",
377
- "stop",
378
- "pause",
379
- "resume",
380
- "complete",
381
- "cancel",
382
- "error",
383
- "capability",
384
- ].includes(kind)
385
- )
386
- return "runstate";
387
- if (
388
- [
389
- "notes",
390
- "scratchpad",
391
- "summary",
392
- "recall",
393
- "correction",
394
- "consentmemory",
395
- "search",
396
- "vault",
397
- "gate",
398
- "grant",
399
- "revoke",
400
- "expiry",
401
- "deny",
402
- ].includes(kind)
403
- )
404
- return "sessions";
405
- if (["configure", "transparency"].includes(kind)) return "settings";
406
- return "logstream";
407
- }
408
-
409
- /** Routes one surface action through the command bus: every action rides the same policy gates before it dispatches and the verdict names the gate that decided. */
410
- export function busrouteaction(
411
- action: surfaceaction,
412
- input: {
413
- sessionactive: boolean;
414
- granted: string[];
415
- planreviewed: boolean;
416
- planstate: string;
417
- text?: string;
418
- origin?: string;
419
- },
420
- ): surfaceroute {
421
- const entry = surfacepalette().find((candidate) => candidate.action.command === action.command);
422
- if (entry === undefined)
423
- return {
424
- dispatched: false,
425
- gate: "commandbus",
426
- reason: `The ${action.surface} asked for the unknown ${action.command} command; the bus routes only catalog commands.`,
427
- };
428
- const permission = paletteactiongate({
429
- action: entry.action,
430
- granted: input.granted,
431
- sessionactive: input.sessionactive,
432
- });
433
- if (!permission.allowed)
434
- return {
435
- dispatched: false,
436
- gate: "paletteactiongate",
437
- reason: permission.reason ?? "The command misses its granted permission.",
438
- };
439
- if (action.command === "starttask") {
440
- const proposal = taskinputproposalgate({ text: input.text ?? "", origin: input.origin ?? "", direct: false });
441
- if (!proposal.allowed)
442
- return {
443
- dispatched: false,
444
- gate: "taskinputproposalgate",
445
- reason: proposal.reason ?? "The task submission refuses.",
446
- };
447
- }
448
- if (action.command === "stepapprove" || action.command === "diffpreview") {
449
- const review = planreviewgate({ reviewed: input.planreviewed, state: input.planstate as "pending" | "approved" });
450
- if (!review.allowed)
451
- return { dispatched: false, gate: "planreviewgate", reason: review.reason ?? "The plan review stays open." };
452
- }
453
- return {
454
- dispatched: true,
455
- gate: "commandbus",
456
- reason: `The ${action.command} action of the ${action.surface} routed through its policy gates and dispatches.`,
457
- };
458
- }
459
-
460
- /** The terminal command registry the cli grows: the palette definitions the extension registers share this registry, so one command catalog drives the surfaces and the cli with no drift between them. */
461
- export function clicommands(
462
- palette: paletteentry[],
463
- ): Array<{ id: string; label: string; keywords: string[]; surface: string; terminal: boolean }> {
464
- const terminal = [
465
- {
466
- id: "manifest",
467
- label: "Validate the extension manifest with the deep manifest checks",
468
- keywords: ["manifest", "permissions", "identity", "validate", "csp", "icons"],
469
- surface: "terminal",
470
- terminal: true,
471
- },
472
- {
473
- id: "describe",
474
- label: "Print the frozen capability manifest of every surface with the protocolv2 negotiation line",
475
- keywords: [
476
- "describe",
477
- "capmanifest",
478
- "surface",
479
- "freeze",
480
- "protocol",
481
- "capabilities",
482
- "messages",
483
- "kinds",
484
- "permissions",
485
- ],
486
- surface: "terminal",
487
- terminal: true,
488
- },
489
- {
490
- id: "commands",
491
- label: "Print the shared command registry of the palette and the cli",
492
- keywords: ["commands", "registry", "palette", "cli", "surface"],
493
- surface: "terminal",
494
- terminal: true,
495
- },
496
- {
497
- id: "planlint",
498
- label: "Lint plan files",
499
- keywords: ["planlint", "lint", "plan", "diagnostics", "rules"],
500
- surface: "terminal",
501
- terminal: true,
502
- },
503
- {
504
- id: "migrateplan",
505
- label: "Convert a foreign plan source into the reviewed plan grammar",
506
- keywords: [
507
- "migrateplan",
508
- "convert",
509
- "import",
510
- "v1",
511
- "automa",
512
- "selenium",
513
- "uivision",
514
- "tabular",
515
- "migration",
516
- "plan",
517
- ],
518
- surface: "terminal",
519
- terminal: true,
520
- },
521
- {
522
- id: "recipes",
523
- label: "List the example gallery and validate one recipe entry",
524
- keywords: ["recipes", "gallery", "examples", "scraping", "forms", "testing", "monitoring", "agents", "dryrun"],
525
- surface: "terminal",
526
- terminal: true,
527
- },
528
- {
529
- id: "flowrun",
530
- label: "Run a plan file",
531
- keywords: ["flowrun", "run", "plan", "terminal", "consent"],
532
- surface: "terminal",
533
- terminal: true,
534
- },
535
- {
536
- id: "runworkflow",
537
- label: "Run a saved workflow with checkpoints and an audit trail file",
538
- keywords: ["runworkflow", "workflow", "replay", "checkpoint", "resume", "dryrun"],
539
- surface: "terminal",
540
- terminal: true,
541
- },
542
- {
543
- id: "exportdata",
544
- label: "Export session, audit or extraction data",
545
- keywords: ["exportdata", "export", "session", "audit", "extraction", "csv", "jsonl", "markdown"],
546
- surface: "terminal",
547
- terminal: true,
548
- },
549
- {
550
- id: "headless",
551
- label: "Replay a plan against recorded page state fixtures",
552
- keywords: ["headless", "fixture", "replay", "recorded", "library"],
553
- surface: "terminal",
554
- terminal: true,
555
- },
556
- {
557
- id: "serve",
558
- label: "Serve the mcp server mode over stdio and a localhost http listener",
559
- keywords: ["serve", "mcp", "model", "context", "protocol", "stdio", "http", "jsonrpc"],
560
- surface: "terminal",
561
- terminal: true,
562
- },
563
- {
564
- id: "native",
565
- label: "Install, remove or diagnose the optional native host of the native bridge",
566
- keywords: ["native", "host", "bridge", "companion", "install", "uninstall", "diagnostics", "messaging"],
567
- surface: "terminal",
568
- terminal: true,
569
- },
570
- {
571
- id: "export",
572
- label: "Export runs, extractions and notes",
573
- keywords: ["export", "csv", "json", "log", "runs", "extractions", "notes"],
574
- surface: "terminal",
575
- terminal: true,
576
- },
577
- {
578
- id: "init",
579
- label: "Scaffold a plan file",
580
- keywords: ["init", "scaffold", "plan", "template"],
581
- surface: "terminal",
582
- terminal: true,
583
- },
584
- {
585
- id: "doctor",
586
- label: "Probe runtime capabilities",
587
- keywords: ["doctor", "capabilities", "runtime", "probe", "matrix"],
588
- surface: "terminal",
589
- terminal: true,
590
- },
591
- {
592
- id: "help",
593
- label: "List every command with the version banner and the exit codes",
594
- keywords: ["help", "usage", "commands", "version"],
595
- surface: "terminal",
596
- terminal: true,
597
- },
598
- ];
599
- const shared = palette.map((entry) => ({
600
- id: entry.id,
601
- label: entry.label,
602
- keywords: entry.keywords,
603
- surface: entry.action.surface,
604
- terminal: false,
605
- }));
606
- return [...terminal, ...shared];
607
- }
608
-
609
- /** The fixed keyboard focus order of the review dialog of the 2.0.2 final polish (roadmap rc.2 item 34): the keyboard reads the step summary first, then walks the approve, revise and reject controls, so the review dialog answers one predictable sequence instead of the tab order the markup happens to carry. */
610
- export const reviewdialogorder: readonly ("readsummary" | "approve" | "revise" | "reject")[] = [
611
- "readsummary",
612
- "approve",
613
- "revise",
614
- "reject",
615
- ];
616
-
617
- /** Computes the ordered tabindex sequence of the review dialog controls: every control of the fixed order earns its tabindex in sequence starting at one while the first control names the focus target the dialog moves focus to when it opens; a control list that repeats or empties a control refuses because a focus order that loses a control hides it from the keyboard. */
618
- export function focusorderof(controls: readonly string[] = reviewdialogorder): {
619
- order: Array<{ control: string; tabindex: number }>;
620
- focusfirst: string;
621
- } {
622
- const seen = new Set<string>();
623
- for (const control of controls) {
624
- if (control.trim() === "") throw new Error("The review dialog focus order needs every control named.");
625
- if (seen.has(control))
626
- throw new Error(
627
- `The review dialog focus order lists the control ${control} twice; one control earns one tabindex.`,
628
- );
629
- seen.add(control);
630
- }
631
- if (controls.length === 0) throw new Error("The review dialog focus order needs its controls.");
632
- const order = controls.map((control, index) => ({ control, tabindex: index + 1 }));
633
- return { order, focusfirst: controls[0]! };
634
- }
635
-
636
- /* ── Merged from statusviews.ts ── */
637
- import { notificationcontentgate } from "./policy.js";
638
-
639
- /**
640
- * Statusbadge, notification, recenttray and stetoast logic of the 1.1.65 family.
641
- * The user stays informed without watching: the statusbadge derives its idle, running, waiting and attention states from the run state and counts the waiting gates when attention is needed on the toolbar icon; notifydone fires on run completion with a deep link to the runsummary while notifyattention fires on gate waits, phishguard blocks and deferrals with a deep link to the exact waiting step and every notification respects the os do not disturb state and shows page content only after its consent; the recenttray lists the latest runs in the popup with resume for halted runs and reopen for completed ones; and the stetoasts confirm step completion with kind and duration, stacking with a bounded live count and a history view.
642
- * No depth or live count is ever hardcoded: the recenttray depth and the toast live count stay user choices with no engine cap, and no notification ever bypasses the content consent.
643
- */
644
-
645
- /** Derives the statusbadge state of the toolbar icon from the run state: idle with no run, running while the approved plan executes, waiting while a gate holds and attention when waiting gates count above zero. */
646
- export function statusbadgeof(input: {
647
- planstate?: "pending" | "approved" | "rejected" | "cancelled" | "expired";
648
- waitingcount: number;
649
- runid?: string;
650
- }): statusbadgestate {
651
- if (input.planstate === undefined) return { state: "idle", waitingcount: 0 };
652
- if (input.waitingcount > 0)
653
- return {
654
- state: "attention",
655
- waitingcount: input.waitingcount,
656
- ...(input.runid !== undefined ? { runid: input.runid } : {}),
657
- };
658
- if (input.planstate === "approved")
659
- return { state: "running", waitingcount: 0, ...(input.runid !== undefined ? { runid: input.runid } : {}) };
660
- if (input.planstate === "pending")
661
- return { state: "waiting", waitingcount: 0, ...(input.runid !== undefined ? { runid: input.runid } : {}) };
662
- return { state: "idle", waitingcount: 0, ...(input.runid !== undefined ? { runid: input.runid } : {}) };
663
- }
664
-
665
- /** Renders the toolbar badge text of one statusbadge state: attention names its waiting gate count, running marks the live run and idle clears the badge. */
666
- export function badgetextof(state: statusbadgestate): string {
667
- if (state.state === "attention") return String(state.waitingcount);
668
- if (state.state === "running") return "run";
669
- if (state.state === "waiting") return "wait";
670
- return "";
671
- }
672
-
673
- /** Names the badge color of one statusbadge state so the outline color follows the run state. */
674
- export function badgecolorof(state: statusbadgestate): string {
675
- if (state.state === "attention") return "#b3261e";
676
- if (state.state === "running") return "#1a73e8";
677
- if (state.state === "waiting") return "#e37400";
678
- return "#5f6368";
679
- }
680
-
681
- /** Pluralizes one counted noun of the notification texts: english and the shipped pt bundle both keep the singular at exactly one while zero and counts above one take the plural — english appends the s while portuguese nouns ending in ão take the ões shape — with the portuguese nouns the notifications name carrying their bundle words so the localized plurals of the 2.0.2 final polish (roadmap rc.2 item 36) read "1 step", "3 steps", "1 etapa", "3 etapas" and "2 execuções" from the same rule. */
682
- export function pluralize(count: number, noun: string, language = "en"): string {
683
- const portuguesenouns: Record<string, string> = {
684
- step: "etapa",
685
- capture: "captura",
686
- run: "execução",
687
- gate: "verificação",
688
- };
689
- const word = language === "pt" ? (portuguesenouns[noun] ?? noun) : noun;
690
- if (count === 1) return word;
691
- if (language === "pt" && word.endsWith("ão")) return `${word.slice(0, -2)}ões`;
692
- return `${word}s`;
693
- }
694
-
695
- /** Renders one counted noun as its localized count label: the number beside its correctly pluralized noun — "1 step", "3 steps", "1 capture", "4 captures" — so every notification body that carries a count reads its own grammar instead of a hardcoded plural. */
696
- export function countlabel(count: number, noun: string, language = "en"): string {
697
- return `${count} ${pluralize(count, noun, language)}`;
698
- }
699
-
700
- /** Builds the notifydone payload of a completed run: the plain language title and body with the deep link to the runsummary; the body never carries page content so no consent is needed, and an absent summary falls back to the counted steps and captures the run recorded, pluralized through the countlabel grammar so one step reads "1 step" while three read "3 steps". */
701
- export function notifydoneof(input: {
702
- runid: string;
703
- origin: string;
704
- summary: string;
705
- steps?: number;
706
- captures?: number;
707
- language?: string;
708
- at: number;
709
- }): notificationpayload {
710
- if (input.runid.trim() === "") throw new Error("The done notification needs its run id.");
711
- const countedsteps = input.steps !== undefined ? ` of the ${countlabel(input.steps, "step", input.language)}` : "";
712
- const countedcaptures =
713
- input.captures !== undefined ? ` beside ${countlabel(input.captures, "capture", input.language)}` : "";
714
- const fallback =
715
- input.steps === undefined && input.captures === undefined
716
- ? `The run of ${input.origin} completed; the runsummary holds every step outcome.`
717
- : `The run of ${input.origin} completed; the runsummary holds every outcome${countedsteps}${countedcaptures}.`;
718
- return {
719
- id: randomid(),
720
- kind: "done",
721
- title: "The run completed",
722
- body: input.summary.trim() === "" ? fallback : input.summary,
723
- deeplink: `#run-${input.runid}`,
724
- runid: input.runid,
725
- content: false,
726
- at: input.at,
727
- };
728
- }
729
-
730
- /** Builds the notifyattention payload of a gate wait, a phishguard block or a deferral: the deep link opens the exact waiting step of the run; a body that carries page content needs its consent before it builds, and an optional waiting gate count appends its pluralized count so "1 gate waits" never reads "1 gates wait". */
731
- export function notifyattentionof(input: {
732
- runid: string;
733
- stepid: string;
734
- cause: "gatewait" | "phishguard" | "deferral";
735
- reason: string;
736
- waitinggates?: number;
737
- language?: string;
738
- content?: boolean;
739
- consent?: boolean;
740
- at: number;
741
- }): notificationpayload {
742
- if (input.stepid.trim() === "") throw new Error("The attention notification needs its waiting step.");
743
- const gate = notificationcontentgate({ content: input.content === true, consent: input.consent === true });
744
- if (!gate.allowed) throw new Error(gate.reason ?? "The attention notification refuses its page content.");
745
- const waiting =
746
- input.waitinggates === undefined ? "" : ` (${countlabel(input.waitinggates, "gate", input.language)} waiting)`;
747
- return {
748
- id: randomid(),
749
- kind: "attention",
750
- title:
751
- input.cause === "gatewait"
752
- ? "The run waits for review"
753
- : input.cause === "phishguard"
754
- ? "The phishguard blocked a step"
755
- : "The run deferred a step",
756
- body: `${input.reason}${waiting}`,
757
- deeplink: `#step-${input.stepid}`,
758
- runid: input.runid,
759
- stepid: input.stepid,
760
- content: input.content === true,
761
- at: input.at,
762
- };
763
- }
764
-
765
- /** Applies the os do not disturb state to one notification: a quiet os suppresses the toast while the deep link stays in the history for the user to open. */
766
- export function notificationrespectsdnd(payload: notificationpayload, dnd: boolean): { show: boolean; reason: string } {
767
- if (dnd)
768
- return {
769
- show: false,
770
- reason: `The os stays in do not disturb, so the ${payload.kind} notification holds its deep link ${payload.deeplink} in the history instead of showing.`,
771
- };
772
- return { show: true, reason: `The ${payload.kind} notification shows with its deep link ${payload.deeplink}.` };
773
- }
774
-
775
- /** Builds one recenttray entry: the run id, the origin, the outcome, the title and the time; a halted run offers resume while a completed run offers reopen. */
776
- export function recenttrayentryof(input: {
777
- runid: string;
778
- origin: string;
779
- outcome: "running" | "completed" | "halted" | "failed";
780
- title: string;
781
- at: number;
782
- }): recenttrayentry {
783
- if (input.runid.trim() === "") throw new Error("The recenttray entry needs its run id.");
784
- return {
785
- runid: input.runid,
786
- origin: input.origin,
787
- outcome: input.outcome,
788
- title: input.title,
789
- at: input.at,
790
- resumable: input.outcome === "halted",
791
- reopenable: input.outcome === "completed" || input.outcome === "failed",
792
- };
793
- }
794
-
795
- /** Appends one recenttray entry with the newest first: the user configured depth keeps the latest runs while an absent depth keeps every run. */
796
- export function recenttrayafter(
797
- entries: recenttrayentry[],
798
- entry: recenttrayentry,
799
- depth: number | undefined,
800
- ): recenttrayentry[] {
801
- const appended = [entry, ...entries.filter((candidate) => candidate.runid !== entry.runid)];
802
- if (depth === undefined) return appended;
803
- if (!Number.isInteger(depth) || depth <= 0) return appended;
804
- return appended.slice(0, depth);
805
- }
806
-
807
- /** Lists the actions one recenttray entry offers: resume for a halted run and reopen for a completed or failed one. */
808
- export function recenttrayactions(entry: recenttrayentry): string[] {
809
- const actions: string[] = [];
810
- if (entry.resumable) actions.push("resume");
811
- if (entry.reopenable) actions.push("reopen");
812
- return actions;
813
- }
814
-
815
- /** Builds one stetoast: the step completion confirmation with the step kind and its duration in milliseconds. */
816
- export function stetoastof(input: { stepid: string; kind: string; durationms: number; at: number }): stetoast {
817
- if (input.stepid.trim() === "") throw new Error("The stetoast needs its step.");
818
- return { id: randomid(), stepid: input.stepid, kind: input.kind, durationms: input.durationms, at: input.at };
819
- }
820
-
821
- /** Stacks one steteoast with a bounded live count: the live stack keeps the newest toasts inside the user configured bound while the full history stays queryable; an absent bound keeps every toast live. */
822
- export function stetoaststackafter(
823
- toasts: stetoast[],
824
- toast: stetoast,
825
- livecount: number | undefined,
826
- ): { live: stetoast[]; history: stetoast[] } {
827
- const history = [...toasts, toast];
828
- if (livecount === undefined || !Number.isInteger(livecount) || livecount <= 0) return { live: history, history };
829
- return { live: history.slice(-livecount), history };
830
- }
831
-
832
- /** Reads the toast history newest first for the history view. */
833
- export function stetoasthistory(toasts: stetoast[]): stetoast[] {
834
- return [...toasts].reverse();
835
- }
836
-
837
- /* ── Merged from pickerviews.ts ── */
838
- import { pickeroverlaygate } from "./policy.js";
839
-
840
- /**
841
- * Pickeroverlay, targethalo, guidedtips and pagechips logic of the 1.1.65 family.
842
- * Selection and inline review live here: the pickeroverlay starts from the sidepanel, lists the element candidates of the granted origin with stability scored selectors and locks one candidate for the proposed step; the targethalo outlines the active target element during a run with its color following the step state; the guidedtips explain the selector choice during picker sessions and dismiss and recall from the optionspage; and the pagechips render inline confirmations anchored to the target element with approve and reject actions whose resolutions write to the immutable log exactly like stepapprove.
843
- * No candidate count or scoring depth is ever hardcoded: every bound stays the user's choice, and no picker read or pagechip resolution ever bypasses the human review.
844
- */
845
-
846
- /** Scores one element candidate for selector stability: an id selector scores highest, stable attribute and role selectors score next, a unique text anchor lifts the score and positional selectors score lowest. */
847
- export function stabilityscoreof(input: {
848
- selector: string;
849
- hasid: boolean;
850
- hasstableattributes: boolean;
851
- hasrole: boolean;
852
- textunique: boolean;
853
- }): number {
854
- let score = 0;
855
- if (input.hasid) score += 40;
856
- if (input.hasstableattributes) score += 25;
857
- if (input.hasrole) score += 15;
858
- if (input.textunique) score += 10;
859
- if (input.selector.trim() === "") score -= 20;
860
- else if (input.selector.includes(":nth-child") || input.selector.includes(":nth-of-type")) score -= 15;
861
- return Math.max(0, Math.min(100, score));
862
- }
863
-
864
- /** Builds one picker candidate with its stability score and the plain language reason the score stands. */
865
- export function pickercandidateof(input: {
866
- selector: string;
867
- text?: string;
868
- role?: string;
869
- hasid: boolean;
870
- hasstableattributes: boolean;
871
- hasrole: boolean;
872
- textunique: boolean;
873
- }): pickercandidate {
874
- const score = stabilityscoreof(input);
875
- const reasons: string[] = [];
876
- if (input.hasid) reasons.push("the id anchors the selector");
877
- if (input.hasstableattributes) reasons.push("stable attributes back the selector");
878
- if (input.hasrole) reasons.push("the aria role names the element");
879
- if (input.textunique) reasons.push("the text stays unique on the page");
880
- if (reasons.length === 0) reasons.push("only the positional shape anchors the selector");
881
- return {
882
- selector: input.selector,
883
- ...(input.text !== undefined && input.text !== "" ? { text: input.text } : {}),
884
- ...(input.role !== undefined && input.role !== "" ? { role: input.role } : {}),
885
- stabilityscore: score,
886
- reason: `The stability score of ${score} stands because ${reasons.join(", ")}.`,
887
- };
888
- }
889
-
890
- /** Starts one pickeroverlay session from the sidepanel: the origin grants gate the reads and the candidates rank by their stability score. */
891
- export function pickersessionstart(input: {
892
- origin: string;
893
- granted: string[];
894
- candidates: pickercandidate[];
895
- at: number;
896
- }): pickersession {
897
- const gate = pickeroverlaygate({ origin: input.origin, granted: input.granted });
898
- if (!gate.allowed) throw new Error(gate.reason);
899
- return { id: randomid(), origin: input.origin, candidates: rankcandidates(input.candidates), startedat: input.at };
900
- }
901
-
902
- /** Ranks the picker candidates by their stability score with the most stable selector first. */
903
- export function rankcandidates(candidates: pickercandidate[]): pickercandidate[] {
904
- return [...candidates].sort((left, right) => right.stabilityscore - left.stabilityscore);
905
- }
906
-
907
- /** Locks one picker candidate for the proposed step: the locked session carries the step id and the chosen selector while a second lock on the same session refuses. */
908
- export function lockcandidate(session: pickersession, candidateindex: number, stepid: string): pickersession {
909
- const candidate = session.candidates[candidateindex];
910
- if (candidate === undefined) throw new Error(`The picker session knows no candidate ${candidateindex} to lock.`);
911
- if (session.lockedstepid !== undefined)
912
- throw new Error(
913
- `The picker session already locks its candidate for the step ${session.lockedstepid}; one session locks one candidate.`,
914
- );
915
- return { ...session, lockedstepid: stepid, lockedselector: candidate.selector };
916
- }
917
-
918
- /** Builds one targethalo geometry descriptor: the step it outlines, the target selector, the pixel rect and the step state that colors the outline. */
919
- export function haloof(input: {
920
- stepid: string;
921
- selector: string;
922
- rect: { x: number; y: number; width: number; height: number };
923
- state: targethalo["state"];
924
- }): targethalo {
925
- if (input.selector.trim() === "") throw new Error("The targethalo needs its target selector.");
926
- return { stepid: input.stepid, selector: input.selector, rect: input.rect, state: input.state };
927
- }
928
-
929
- /** Colors the targethalo outline by step state: pending stays gray, running blue, waiting amber, done green, failed red and halted dark. */
930
- export function halocolorof(state: targethalo["state"]): string {
931
- if (state === "running") return "#1a73e8";
932
- if (state === "waiting") return "#e37400";
933
- if (state === "done") return "#188038";
934
- if (state === "failed") return "#b3261e";
935
- if (state === "halted") return "#3c4043";
936
- return "#5f6368";
937
- }
938
-
939
- /** The guidedtips content bound to picker sessions: each tip explains one selector choice with its surface and its picker step. */
940
- export function guidedtips(): guidedtip[] {
941
- return [
942
- {
943
- id: "selectorstability",
944
- surface: "sidepanel",
945
- title: "Selector stability",
946
- body: "Devthink scores every candidate selector by its stability: an id anchor, stable attributes, an aria role and a unique text each lift the score while a positional shape lowers it, so the proposed step binds to the selector least likely to break.",
947
- pickerstep: "candidatepick",
948
- },
949
- {
950
- id: "candidatelock",
951
- surface: "sidepanel",
952
- title: "Locking a candidate",
953
- body: "Lock one candidate to bind it to the proposed step; one picker session locks one candidate and the locked selector rides the step for its review.",
954
- pickerstep: "candidatelock",
955
- },
956
- {
957
- id: "haloreadout",
958
- surface: "sidepanel",
959
- title: "The halo read out",
960
- body: "During a run the targethalo outlines the active target element and its color follows the step state: gray while pending, blue while running, amber at a gate, green when done, red on failure and dark when halted.",
961
- pickerstep: "halotracking",
962
- },
963
- ];
964
- }
965
-
966
- /** Dismisses one guidedtip: the dismissed tip never shows again on its own while the optionspage recalls every dismissed tip on demand. */
967
- export function guidedtipdismiss(tips: guidedtip[], dismissed: string[], tipid: string): string[] {
968
- const tip = tips.find((candidate) => candidate.id === tipid);
969
- if (tip === undefined) throw new Error(`The guidedtips know no ${tipid} tip.`);
970
- return [...new Set([...dismissed, tipid])];
971
- }
972
-
973
- /** Recalls every dismissed guidedtip: the recall clears the dismissed list so the tips show again. */
974
- export function guidedtiprecall(dismissed: string[]): string[] {
975
- return [];
976
- }
977
-
978
- /** Builds one pagechip confirmation anchored to the target element of a gated step: the chip renders inline on the page with its approve and reject actions. */
979
- export function pagechipof(input: {
980
- stepid: string;
981
- selector: string;
982
- origin: string;
983
- at: number;
984
- }): pagechipconfirmation {
985
- if (input.stepid.trim() === "") throw new Error("The pagechip needs its step.");
986
- if (input.selector.trim() === "") throw new Error("The pagechip needs its anchor selector.");
987
- return { id: randomid(), stepid: input.stepid, selector: input.selector, origin: input.origin, at: input.at };
988
- }
989
-
990
- /** Resolves one pagechip with its approve or reject action: the resolution carries its human surface and writes to the immutable log exactly like a stepapprove resolution. */
991
- export function pagechipresolve(
992
- chip: pagechipconfirmation,
993
- resolution: "approve" | "reject",
994
- surface: uisurface | "background",
995
- at: number,
996
- ): { chip: pagechipconfirmation; logevent: { kind: "review"; summary: string; stepid: string } } {
997
- if (surface === "background")
998
- throw new Error(
999
- "The pagechip resolution needs its distinct human action from a surface; the background never resolves a review on its own.",
1000
- );
1001
- const resolved: pagechipconfirmation = { ...chip, resolution, resolvedat: at };
1002
- return {
1003
- chip: resolved,
1004
- logevent: {
1005
- kind: "review",
1006
- stepid: chip.stepid,
1007
- summary: `The user ${resolution === "approve" ? "approved" : "rejected"} the step ${chip.stepid} of ${chip.origin} from the pagechip anchored to ${chip.selector} on the ${surface}; one distinct human action resolved the step alone.`,
1008
- },
1009
- };
1010
- }
1011
-
1012
- /* ── Merged from tourviews.ts ── */
1013
-
1014
- /**
1015
- * Featuretour and a11ylabels logic of the 1.1.65 family.
1016
- * The interface teaches itself here: the featuretour replays the onboarding walkthrough on demand and adds its own stops for the datagrid, the compareviewer and the pickeroverlay with the surface and the focus of every stop; and the a11ylabels name every control across the popup, the sidepanel, the dashboardpage and the optionspage with role, name, state and value for screen readers, following the language of the interface.
1017
- * No stop count or label set is ever hardcoded: every tour and label reads the same locale bundles, and no tour stop ever bypasses the human review.
1018
- */
1019
-
1020
- /** The featuretour stops: the onboarding walkthrough replays on demand while the added stops walk the datagrid, the compareviewer and the pickeroverlay with their surface and focus. */
1021
- export function featuretourstops(): featuretourstop[] {
1022
- return [
1023
- {
1024
- id: "origingrants",
1025
- surface: "popup",
1026
- focus: "#allowlist",
1027
- title: "Origin grants",
1028
- body: "Devthink denies automation by default; grant one exact origin at a time and every run stays inside the granted origins.",
1029
- order: 1,
1030
- },
1031
- {
1032
- id: "planreview",
1033
- surface: "sidepanel",
1034
- focus: "#plancards",
1035
- title: "Plan review",
1036
- body: "Every task becomes a plan of reviewed steps; read the plancards of each risk class and approve, reject or edit one step at a time.",
1037
- order: 2,
1038
- },
1039
- {
1040
- id: "runcontrol",
1041
- surface: "sidepanel",
1042
- focus: "#timeline",
1043
- title: "Run control",
1044
- body: "Runs start, pause, resume and cancel under your hand while the stepstimeline follows every transition.",
1045
- order: 3,
1046
- },
1047
- {
1048
- id: "logaudit",
1049
- surface: "dashboardpage",
1050
- focus: "#sessiongrid",
1051
- title: "Log audit",
1052
- body: "The immutable log chains every step transition with masked values; verify the chain and copy a verified range as an audit excerpt.",
1053
- order: 4,
1054
- },
1055
- {
1056
- id: "datagrid",
1057
- surface: "sidepanel",
1058
- focus: "#datagrid",
1059
- title: "The datagrid",
1060
- body: "Extraction results render as a grid with inferred column types; sort and filter locally, select a row range and export csv, json or clipboard with masked values only.",
1061
- order: 5,
1062
- },
1063
- {
1064
- id: "compareviewer",
1065
- surface: "sidepanel",
1066
- focus: "#compareviewer",
1067
- title: "The compareviewer",
1068
- body: "Every executed write step pairs its before and after captures; the slider overlays the two so you see exactly what the step changed.",
1069
- order: 6,
1070
- },
1071
- {
1072
- id: "pickeroverlay",
1073
- surface: "sidepanel",
1074
- focus: "#picker",
1075
- title: "The pickeroverlay",
1076
- body: "Start a picker session to list the element candidates of the granted origin with stability scored selectors; lock one candidate for the proposed step.",
1077
- order: 7,
1078
- },
1079
- {
1080
- id: "chunkextract",
1081
- surface: "sidepanel",
1082
- focus: "#datagrid",
1083
- title: "Chunked extraction",
1084
- body: "Big tables extract in resumable row windows: the datagrid appends every window as it arrives, the cursor progress line shows the rows extracted so far, and an interruption resumes from the fingerprint verified cursor.",
1085
- order: 8,
1086
- },
1087
- ];
1088
- }
1089
-
1090
- /** Orders the featuretour stops by their stop order for the replay. */
1091
- export function featuretourordered(stops: featuretourstop[]): featuretourstop[] {
1092
- return [...stops].sort((left, right) => left.order - right.order);
1093
- }
1094
-
1095
- /** The stop of the featuretour at one position for the replay walkthrough. */
1096
- export function featuretourstopat(stops: featuretourstop[], position: number): featuretourstop | undefined {
1097
- return featuretourordered(stops)[position];
1098
- }
1099
-
1100
- /** Builds one a11ylabel: the control, its role, its accessible name and its optional state and value for screen readers. */
1101
- export function a11ylabelof(input: {
1102
- control: string;
1103
- role: a11ylabel["role"];
1104
- name: string;
1105
- state?: string;
1106
- value?: string;
1107
- }): a11ylabel {
1108
- if (input.control.trim() === "") throw new Error("The a11ylabel needs its control.");
1109
- if (input.name.trim() === "") throw new Error("The a11ylabel needs its accessible name.");
1110
- return {
1111
- control: input.control,
1112
- role: input.role,
1113
- name: input.name,
1114
- ...(input.state !== undefined ? { state: input.state } : {}),
1115
- ...(input.value !== undefined ? { value: input.value } : {}),
1116
- };
1117
- }
1118
-
1119
- /** The a11ylabels of one surface: every control across the popup, the sidepanel, the dashboardpage and the optionspage carries its role, name, state and value. */
1120
- export function a11ylabelsfor(surface: uisurface): a11ylabel[] {
1121
- const labels: Record<uisurface, a11ylabel[]> = {
1122
- popup: [
1123
- a11ylabelof({ control: "taskinput", role: "textbox", name: "popup.taskinput.placeholder", state: "idle" }),
1124
- a11ylabelof({ control: "submit", role: "button", name: "popup.taskinput.submit" }),
1125
- a11ylabelof({ control: "palette", role: "button", name: "popup.palette.open" }),
1126
- a11ylabelof({ control: "recenttray", role: "list", name: "popup.recent.title", value: "0 runs" }),
1127
- ],
1128
- sidepanel: [
1129
- a11ylabelof({ control: "plantab", role: "tab", name: "sidepanel.tab.plan", state: "selected" }),
1130
- a11ylabelof({ control: "runtab", role: "tab", name: "sidepanel.tab.run", state: "unselected" }),
1131
- a11ylabelof({ control: "reviewtab", role: "tab", name: "sidepanel.tab.review", state: "unselected" }),
1132
- a11ylabelof({ control: "datagrid", role: "table", name: "grid.empty" }),
1133
- a11ylabelof({ control: "compareviewer", role: "slider", name: "sidepanel.data.compare", value: "50" }),
1134
- a11ylabelof({ control: "picker", role: "button", name: "sidepanel.data.picker" }),
1135
- ],
1136
- dashboardpage: [
1137
- a11ylabelof({ control: "sessiongrid", role: "table", name: "dashboard.title", value: "0 runs" }),
1138
- a11ylabelof({ control: "historysearch", role: "search", name: "dashboard.history" }),
1139
- a11ylabelof({ control: "dropzone", role: "region", name: "options.importexport.label" }),
1140
- ],
1141
- optionspage: [
1142
- a11ylabelof({ control: "theme", role: "radiogroup", name: "options.theme.label", value: "system" }),
1143
- a11ylabelof({ control: "locale", role: "combobox", name: "options.locale.label", value: "en" }),
1144
- a11ylabelof({ control: "shortcuts", role: "group", name: "options.shortcuts.label" }),
1145
- a11ylabelof({ control: "notifications", role: "switch", name: "options.notifications.label", state: "off" }),
1146
- a11ylabelof({ control: "importexport", role: "region", name: "options.importexport.label" }),
1147
- a11ylabelof({ control: "tour", role: "button", name: "options.tour.label" }),
1148
- ],
1149
- onboarding: [a11ylabelof({ control: "onboarding", role: "dialog", name: "options.tour.label", state: "open" })],
1150
- omnibox: [a11ylabelof({ control: "omnibox", role: "textbox", name: "popup.taskinput.placeholder" })],
1151
- page: [a11ylabelof({ control: "pagechip", role: "group", name: "pagechip.approve", state: "pending" })],
1152
- };
1153
- return labels[surface];
1154
- }
1155
-
1156
- /** Localizes one a11ylabel through the locale bundles: the accessible name resolves in the language of the interface with the english fallback so every control keeps a spoken name. */
1157
- export function a11ylabellocalized(label: a11ylabel, bundles: localebundle[], language: string): a11ylabel {
1158
- return { ...label, name: localestring(bundles, language, label.name) };
1159
- }
1160
-
1161
- /** Localizes every a11ylabel of one surface so screen readers follow the language of the interface. */
1162
- export function a11ylabelslocalizedfor(surface: uisurface, bundles: localebundle[], language: string): a11ylabel[] {
1163
- return a11ylabelsfor(surface).map((label) => a11ylabellocalized(label, bundles, language));
1164
- }
1165
-
1166
- /* ── Merged from evidenceviews.ts ── */
1167
- import { shotpanelgate } from "./policy.js";
1168
-
1169
- /**
1170
- * Shotpanel and compareviewer logic of the 1.1.65 family.
1171
- * Evidence views live here: the shotpanel previews the capture of every step with its capture provenance and redaction verdicts and zooms and pans the large stitched captures; the compareviewer pairs the before and after captures of every executed write step and overlays the two captures with a slider so the user sees exactly what the step changed.
1172
- * No zoom factor, pan step or pixel ceiling is ever hardcoded: every view stays a user choice, and no capture of an ungranted origin ever opens in the panel.
1173
- */
1174
-
1175
- /** Builds one shotpanel view: the step, the run, the capture id, the capture provenance, the redaction verdicts and the zoom and pan the user set; the capture origin must hold a granted origin. */
1176
- export function shotpanelof(input: {
1177
- stepid: string;
1178
- runid: string;
1179
- captureid: string;
1180
- provenance: "viewport" | "fullpage" | "element" | "region";
1181
- origin: string;
1182
- granted: string[];
1183
- redactions?: Array<{ region: string; verdict: string }>;
1184
- at: number;
1185
- }): shotpanelview {
1186
- const gate = shotpanelgate({ captureorigin: input.origin, granted: input.granted });
1187
- if (!gate.allowed) throw new Error(gate.reason);
1188
- if (input.stepid.trim() === "") throw new Error("The shotpanel view needs its step.");
1189
- return {
1190
- id: randomid(),
1191
- stepid: input.stepid,
1192
- runid: input.runid,
1193
- captureid: input.captureid,
1194
- provenance: input.provenance,
1195
- origin: input.origin,
1196
- redactions: input.redactions ?? [],
1197
- zoom: 1,
1198
- pan: { x: 0, y: 0 },
1199
- at: input.at,
1200
- };
1201
- }
1202
-
1203
- /** Zooms one shotpanel view: the factor multiplies the current zoom while a non positive zoom refuses the change. */
1204
- export function shotpanelzoom(view: shotpanelview, factor: number): shotpanelview {
1205
- if (!(factor > 0)) throw new Error("The shotpanel zoom factor stays a positive number.");
1206
- return { ...view, zoom: view.zoom * factor };
1207
- }
1208
-
1209
- /** Pans one shotpanel view: the offsets move the viewport over the large stitched capture so the user inspects any region. */
1210
- export function shotpanelpan(view: shotpanelview, offset: { x: number; y: number }): shotpanelview {
1211
- return { ...view, pan: { x: view.pan.x + offset.x, y: view.pan.y + offset.y } };
1212
- }
1213
-
1214
- /** Builds one compareviewer pair: the step, the before capture, the after capture and the slider that starts at the middle so the overlay splits the two captures. */
1215
- export function comparepairof(input: {
1216
- stepid: string;
1217
- beforecaptureid: string;
1218
- aftercaptureid: string;
1219
- }): compareviewerpair {
1220
- if (input.stepid.trim() === "") throw new Error("The compareviewer pair needs its step.");
1221
- if (input.beforecaptureid === input.aftercaptureid)
1222
- throw new Error("The compareviewer pair needs its distinct before and after captures.");
1223
- return {
1224
- id: randomid(),
1225
- stepid: input.stepid,
1226
- beforecaptureid: input.beforecaptureid,
1227
- aftercaptureid: input.aftercaptureid,
1228
- slidervalue: 50,
1229
- };
1230
- }
1231
-
1232
- /** Moves the compareviewer slider: the value stays between zero and one hundred so the overlay splits the before and after captures at the user chosen position. */
1233
- export function overlayslider(pair: compareviewerpair, value: number): compareviewerpair {
1234
- if (!Number.isFinite(value) || value < 0 || value > 100)
1235
- throw new Error("The compareviewer slider stays between zero and one hundred.");
1236
- return { ...pair, slidervalue: value };
1237
- }
1238
-
1239
- /** Pairs the before and after captures of every executed write step: only a step that changed page or browser state earns a pair, and each pair names its step. */
1240
- export function comparepairsforsteps(
1241
- steps: Array<{ stepid: string; risk: "read" | "interaction" | "sensitive"; writeexecuted: boolean }>,
1242
- captures: Record<string, { beforecaptureid?: string; aftercaptureid?: string }>,
1243
- ): compareviewerpair[] {
1244
- const pairs: compareviewerpair[] = [];
1245
- for (const step of steps) {
1246
- if (!step.writeexecuted) continue;
1247
- const capture = captures[step.stepid];
1248
- if (capture?.beforecaptureid === undefined || capture?.aftercaptureid === undefined) continue;
1249
- pairs.push(
1250
- comparepairof({
1251
- stepid: step.stepid,
1252
- beforecaptureid: capture.beforecaptureid,
1253
- aftercaptureid: capture.aftercaptureid,
1254
- }),
1255
- );
1256
- }
1257
- return pairs;
1258
- }
1259
-
1260
- /* ── Merged from datagrid.ts ── */
1261
-
1262
- /**
1263
- * Datagrid and exportmenu logic of the 1.1.65 family.
1264
- * The extraction half of the interface lives here: a datagrid view arranges the extraction result of one run into columns and rows with the column types inferred from the values themselves, sorts and filters rows locally inside the surface, and selects a row range so a partial export ships only what the user chose; the exportmenu offers csv, json and clipboard formats scoped to the selection, one step or the whole run and writes masked values only, honoring the maskinputs verdicts of every sensitive field shape so no clear value ever leaves the grid.
1265
- * No row count, column count or byte ceiling is ever hardcoded: every bound stays the user's choice with no engine cap, and no export path ever bypasses the mask verdicts.
1266
- */
1267
-
1268
- /** Infers one column type from its observed values: numbers, booleans and iso dates infer their type while every other shape stays text; an all empty column infers empty. */
1269
- export function infercolumntype(values: string[]): datagridcolumn["type"] {
1270
- const present = values.filter((value) => value.trim() !== "");
1271
- if (present.length === 0) return "empty";
1272
- if (present.every((value) => /^-?\d+(?:\.\d+)?$/.test(value.trim()))) return "number";
1273
- if (present.every((value) => value.trim() === "true" || value.trim() === "false")) return "boolean";
1274
- if (present.every((value) => !Number.isNaN(Date.parse(value.trim())) && /\d{4}-\d{2}-\d{2}/.test(value.trim())))
1275
- return "date";
1276
- return "text";
1277
- }
1278
-
1279
- /** Builds the inferred columns of one extraction result: every field of every row becomes one column with its type inferred from the observed values. */
1280
- export function datagridcolumnsof(rows: Array<Record<string, string>>): datagridcolumn[] {
1281
- const fields = [...new Set(rows.flatMap((row) => Object.keys(row)))];
1282
- return fields.map((field) => ({
1283
- field,
1284
- label: field,
1285
- type: infercolumntype(rows.map((row) => row[field] ?? "")),
1286
- inferred: true,
1287
- }));
1288
- }
1289
-
1290
- /** Builds one datagrid view of an extraction result: the title, the origin it ran on, the run it belongs to, the inferred columns and the rows with their original order as index. */
1291
- export function datagridof(input: {
1292
- title: string;
1293
- origin: string;
1294
- runid: string;
1295
- rows: Array<Record<string, string>>;
1296
- at: number;
1297
- }): datagridview {
1298
- if (input.title.trim() === "") throw new Error("The datagrid view needs its title.");
1299
- if (input.origin.trim() === "") throw new Error("The datagrid view needs its origin.");
1300
- if (input.rows.length === 0) throw new Error("The datagrid view needs at least one extracted row.");
1301
- const columns = datagridcolumnsof(input.rows);
1302
- const rows: datagridrow[] = input.rows.map((row, index) => ({
1303
- index,
1304
- values: Object.fromEntries(columns.map((column) => [column.field, row[column.field] ?? ""])),
1305
- }));
1306
- return {
1307
- id: randomid(),
1308
- title: input.title.trim(),
1309
- origin: input.origin.trim(),
1310
- runid: input.runid,
1311
- columns,
1312
- rows,
1313
- at: input.at,
1314
- };
1315
- }
1316
-
1317
- /** Sorts the datagrid rows locally by one column: text sorts lexicographically while numbers, booleans and dates sort by their parsed value; an absent direction keeps the original order. */
1318
- export function sortdatagridrows(
1319
- view: datagridview,
1320
- input: { field: string; direction: "ascending" | "descending" },
1321
- ): datagridview {
1322
- const column = view.columns.find((candidate) => candidate.field === input.field);
1323
- if (column === undefined) throw new Error(`The datagrid knows no ${input.field} column to sort.`);
1324
- const rows = [...view.rows]
1325
- .sort((left, right) => {
1326
- const leftvalue = left.values[input.field] ?? "";
1327
- const rightvalue = right.values[input.field] ?? "";
1328
- let compared = 0;
1329
- if (column.type === "number") compared = Number(leftvalue) - Number(rightvalue);
1330
- else if (column.type === "boolean") compared = (leftvalue === "true" ? 1 : 0) - (rightvalue === "true" ? 1 : 0);
1331
- else if (column.type === "date") compared = Date.parse(leftvalue) - Date.parse(rightvalue);
1332
- else compared = leftvalue.localeCompare(rightvalue);
1333
- return input.direction === "descending" ? -compared : compared;
1334
- })
1335
- .map((row, index) => ({ ...row, index }));
1336
- return { ...view, rows };
1337
- }
1338
-
1339
- /** Filters the datagrid rows locally by one text query: a row stays when any column value contains the query; an empty query keeps every row. */
1340
- export function filterdatagridrows(view: datagridview, text: string): datagridview {
1341
- const query = text.trim().toLowerCase();
1342
- if (query === "") return view;
1343
- const rows = view.rows.filter((row) =>
1344
- Object.values(row.values).some((value) => value.toLowerCase().includes(query)),
1345
- );
1346
- return { ...view, rows };
1347
- }
1348
-
1349
- /** Selects one inclusive row range for a partial export: the rows inside the range carry their selected mark while the rest clears. */
1350
- export function selectrowrange(view: datagridview, from: number, to: number): datagridview {
1351
- if (from < 0 || to < from || to >= view.rows.length)
1352
- throw new Error(
1353
- `The row range ${from} to ${to} names no inclusive slice of the ${view.rows.length} row${view.rows.length === 1 ? "" : "s"}.`,
1354
- );
1355
- const rows = view.rows.map((row) => ({ ...row, selected: row.index >= from && row.index <= to }));
1356
- return { ...view, rows };
1357
- }
1358
-
1359
- /** The rows one export ships: a selection scope keeps the selected rows only while the step and run scopes ship every row of the grid. */
1360
- export function exportrowsof(view: datagridview, scope: exportmenudescriptor["scope"]): datagridrow[] {
1361
- if (scope === "selection") {
1362
- const selected = view.rows.filter((row) => row.selected === true);
1363
- if (selected.length === 0)
1364
- throw new Error("The selection export needs its selected row range; select rows before the export.");
1365
- return selected;
1366
- }
1367
- return view.rows;
1368
- }
1369
-
1370
- /** Builds the exportmenu descriptors of one datagrid: the csv, json and clipboard formats scoped to the selection, one step or the whole run. */
1371
- export function exportmenudescriptors(): exportmenudescriptor[] {
1372
- return (["csv", "json", "clipboard"] as const).flatMap((format) =>
1373
- (["selection", "step", "run"] as const).map((scope) => ({
1374
- format,
1375
- scope,
1376
- destination: format === "clipboard" ? "clipboard" : "download",
1377
- })),
1378
- );
1379
- }
1380
-
1381
- /** Masks one field value when the mask verdicts name it: a masked value never renders in the clear while every other value ships as extracted. */
1382
- export function maskedvalueof(
1383
- value: string,
1384
- field: string,
1385
- maskverdicts: Record<string, string>,
1386
- ): { value: string; masked: boolean } {
1387
- const verdict = maskverdicts[field];
1388
- if (verdict === undefined) return { value, masked: false };
1389
- return { value: `${"•".repeat(Math.min(value.length, 8))} (${value.length} characters, masked)`, masked: true };
1390
- }
1391
-
1392
- /** Escapes one csv field: a value with a quote, a comma or a newline wraps in quotes with its quotes doubled. */
1393
- function csvfield(value: string): string {
1394
- if (/[",\n]/.test(value)) return `"${value.replaceAll('"', '""')}"`;
1395
- return value;
1396
- }
1397
-
1398
- /** Renders the export payload of one datagrid view: csv writes the header row and every shipping row, json writes the typed records and the clipboard format writes the csv text for the clipboard. */
1399
- export function exportdatagrid(
1400
- view: datagridview,
1401
- descriptor: exportmenudescriptor,
1402
- maskverdicts: Record<string, string> = {},
1403
- ): {
1404
- format: exportmenudescriptor["format"];
1405
- scope: exportmenudescriptor["scope"];
1406
- destination: exportmenudescriptor["destination"];
1407
- text: string;
1408
- rows: number;
1409
- maskedfields: string[];
1410
- } {
1411
- const rows = exportrowsof(view, descriptor.scope);
1412
- const maskedfields = [
1413
- ...new Set(rows.flatMap((row) => Object.keys(row.values)).filter((field) => maskverdicts[field] !== undefined)),
1414
- ];
1415
- if (descriptor.format === "json") {
1416
- const records = rows.map((row) =>
1417
- Object.fromEntries(
1418
- view.columns.map((column) => {
1419
- const masked = maskedvalueof(row.values[column.field] ?? "", column.field, maskverdicts);
1420
- return [
1421
- column.field,
1422
- column.type === "number" && !masked.masked
1423
- ? Number(masked.value)
1424
- : column.type === "boolean" && !masked.masked
1425
- ? masked.value === "true"
1426
- : masked.value,
1427
- ];
1428
- }),
1429
- ),
1430
- );
1431
- return {
1432
- format: descriptor.format,
1433
- scope: descriptor.scope,
1434
- destination: descriptor.destination,
1435
- text: JSON.stringify(
1436
- {
1437
- view: view.title,
1438
- origin: view.origin,
1439
- runid: view.runid,
1440
- columns: view.columns.map((column) => ({ field: column.field, type: column.type })),
1441
- rows: records,
1442
- },
1443
- null,
1444
- 2,
1445
- ),
1446
- rows: records.length,
1447
- maskedfields,
1448
- };
1449
- }
1450
- const header = view.columns.map((column) => csvfield(column.label)).join(",");
1451
- const lines = rows.map((row) =>
1452
- view.columns
1453
- .map((column) => csvfield(maskedvalueof(row.values[column.field] ?? "", column.field, maskverdicts).value))
1454
- .join(","),
1455
- );
1456
- return {
1457
- format: descriptor.format,
1458
- scope: descriptor.scope,
1459
- destination: descriptor.destination,
1460
- text: [header, ...lines].join("\n"),
1461
- rows: rows.length,
1462
- maskedfields,
1463
- };
1464
- }
1465
-
1466
- /* ── Merged from virtlist.ts ── */
1467
-
1468
- /**
1469
- * Virtlist logic of the 1.1.68 family.
1470
- * Long lists render through a virtualized window in every surface: the window keeps the visible row range, the measured height map holds the row heights the measurement pass records, and the row nodes recycle during scroll instead of rebuilding.
1471
- * The row window stays the user's choice: an absent window renders every row, and no engine default ever caps a list.
1472
- */
1473
-
1474
- /** Builds the initial virtlist window of one surface at scroll top: the window starts at row zero, spans the user configured row count or every row when the user set none, and the height map starts empty. */
1475
- export function openvirtlist(input: { surface: string; total: number; rows?: number }): virtlistwindow {
1476
- if (input.surface.trim() === "") throw new Error("The virtlist window needs its surface name.");
1477
- const span = input.rows === undefined ? input.total : Math.min(input.rows, Math.max(input.total, 0));
1478
- return { surface: input.surface, start: 0, end: span, total: input.total, heights: {}, recycled: 0 };
1479
- }
1480
-
1481
- /** Moves one virtlist window to a scroll offset: the window keeps its span, clamps inside the row range and recycles the rows the previous window rendered when they stay inside the new range. */
1482
- export function scrollvirtlist(window: virtlistwindow, start: number, rows?: number): virtlistwindow {
1483
- const span = rows ?? window.end - window.start;
1484
- const clamped = Math.max(0, Math.min(start, Math.max(window.total - span, 0)));
1485
- const end = Math.min(clamped + span, window.total);
1486
- const previous = new Set(range(window.start, window.end));
1487
- const current = new Set(range(clamped, end));
1488
- let recycled = 0;
1489
- for (const row of current) if (previous.has(row)) recycled += 1;
1490
- return { ...window, start: clamped, end, recycled };
1491
- }
1492
-
1493
- /** Measures the row heights of one virtlist window: the height map records the measured height of every rendered row while the rows outside the window keep their last measurement. */
1494
- export function measurevirtlistrows(window: virtlistwindow, measurements: Record<string, number>): virtlistwindow {
1495
- return { ...window, heights: { ...window.heights, ...measurements } };
1496
- }
1497
-
1498
- /** Reads the row slice one virtlist window renders: only the rows inside the window travel to the surface while the full list stays in memory. */
1499
- export function virtlistrows<T>(window: virtlistwindow, rows: T[]): T[] {
1500
- return rows.slice(window.start, window.end);
1501
- }
1502
-
1503
- /** Reads the estimated height of one virtlist window from its height map: measured rows count their measurement while unmeasured rows count the estimate the caller supplies because no engine default exists. */
1504
- export function virtlistheight(
1505
- window: virtlistwindow,
1506
- estimate: number,
1507
- ): { measured: number; estimate: number; total: number } {
1508
- const measured = Object.values(window.heights).reduce((sum, height) => sum + height, 0);
1509
- const unmeasured = Math.max(window.total - Object.keys(window.heights).length, 0);
1510
- return { measured, estimate, total: measured + unmeasured * estimate };
1511
- }
1512
-
1513
- /** Reads the render savings of one virtlist window: the rows the surface avoided rendering beside the recycled nodes the scroll reused. */
1514
- export function virtlistsavings(window: virtlistwindow): { rendered: number; skipped: number; recycled: number } {
1515
- const rendered = Math.max(window.end - window.start, 0);
1516
- return { rendered, skipped: Math.max(window.total - rendered, 0), recycled: window.recycled };
1517
- }
1518
-
1519
- /** Builds the height map key of one row so the per surface height maps the memory stores stay stable across scrolls. */
1520
- export function heightmapkey(surface: string, row: number): string {
1521
- return `${surface}:${row}`;
1522
- }
1523
-
1524
- /** Builds the per surface height map record the memory stores for reuse: the surface, its measured heights and the time of the measurement pass. */
1525
- export function heightmaprecord(input: { surface: string; heights: Record<string, number>; now: number }): {
1526
- surface: string;
1527
- heights: Record<string, number>;
1528
- at: number;
1529
- } {
1530
- if (input.surface.trim() === "") throw new Error("The height map needs its surface name.");
1531
- return { surface: input.surface, heights: { ...input.heights }, at: input.now };
1532
- }
1533
-
1534
- /** Exclusive row range helper of the virtualized windows. */
1535
- function range(start: number, end: number): number[] {
1536
- const rows: number[] = [];
1537
- for (let row = start; row < end; row += 1) rows.push(row);
1538
- return rows;
1539
- }
1540
-
1541
- /* ── Merged from siteprefs.ts ── */
1542
- import { siteprofilegate } from "./policy.js";
1543
-
1544
- /**
1545
- * Siteprofiles, darklight themes and locale logic of the 1.1.65 family.
1546
- * The surface rounds out here: siteprofiles store the per site interface preferences (the theme, the shortcutkeys and the default view) beside the originprofiles policy preferences and activate automatically on their origin; the darklight theme follows the os preference with a manual override while its tokens cover every surface including the dashboardpage; and the locale bundles ship the interface strings of every supported language with an english fallback for missing strings and per language date, number and duration formatting.
1547
- * No theme, language or profile is ever forced: every preference stays the user's choice, and no siteprofile ever adjusts a policy gate.
1548
- */
1549
-
1550
- /** Builds one siteprofile: the origin, the theme, the shortcutkeys, the default view and the update time; the origin must hold an https shape because the profile extends the originprofiles family. */
1551
- export function siteprofileof(input: {
1552
- origin: string;
1553
- theme?: siteprofile["theme"];
1554
- shortcuts?: shortcutbinding[];
1555
- defaultview?: string;
1556
- at: number;
1557
- }): siteprofile {
1558
- const gate = siteprofilegate({ origin: input.origin });
1559
- if (!gate.allowed) throw new Error(gate.reason);
1560
- return {
1561
- origin: input.origin,
1562
- ...(input.theme !== undefined ? { theme: input.theme } : {}),
1563
- ...(input.shortcuts !== undefined ? { shortcuts: input.shortcuts } : {}),
1564
- ...(input.defaultview !== undefined ? { defaultview: input.defaultview } : {}),
1565
- updatedat: input.at,
1566
- };
1567
- }
1568
-
1569
- /** Tests whether one siteprofile activates on the given origin: the profile applies automatically when its origin matches the active origin exactly. */
1570
- export function siteprofileactive(profile: siteprofile, origin: string): boolean {
1571
- return profile.origin === origin;
1572
- }
1573
-
1574
- /** Resolves the effective siteprofile of one origin: the stored profile applies when its origin matches while an absent profile returns undefined and the global preferences keep governing. */
1575
- export function siteprofilefor(profiles: siteprofile[], origin: string): siteprofile | undefined {
1576
- return profiles.find((profile) => siteprofileactive(profile, origin));
1577
- }
1578
-
1579
- /** The darklight theme tokens of one mode: every surface, including the dashboardpage, reads the same token names so the theme covers the whole interface. */
1580
- export function darklighttokensof(mode: darklighttokens["mode"]): darklighttokens {
1581
- const tokens: Record<string, string> =
1582
- mode === "dark"
1583
- ? {
1584
- surface: "#1f1f1f",
1585
- elevated: "#2b2b2b",
1586
- text: "#e3e3e3",
1587
- muted: "#9aa0a6",
1588
- accent: "#8ab4f8",
1589
- border: "#3c4043",
1590
- focus: "#aecbfa",
1591
- error: "#f28b82",
1592
- success: "#81c995",
1593
- warning: "#fdd663",
1594
- }
1595
- : {
1596
- surface: "#ffffff",
1597
- elevated: "#f8f9fa",
1598
- text: "#202124",
1599
- muted: "#5f6368",
1600
- accent: "#1a73e8",
1601
- border: "#dadce0",
1602
- focus: "#174ea6",
1603
- error: "#b3261e",
1604
- success: "#188038",
1605
- warning: "#e37400",
1606
- };
1607
- return { mode, tokens };
1608
- }
1609
-
1610
- /** Resolves the effective appearance: the siteprofile theme wins on its origin, the manual user override wins next and the os preference governs when no override exists, with the source naming which layer decided. */
1611
- export function resolveappearance(input: {
1612
- ospreference: "dark" | "light";
1613
- useroverride?: "dark" | "light" | "system";
1614
- siteprofile?: siteprofile;
1615
- }): darklighttokens {
1616
- if (input.siteprofile?.theme !== undefined && input.siteprofile.theme !== "system")
1617
- return { ...darklighttokensof(input.siteprofile.theme), source: "site" };
1618
- if (input.useroverride !== undefined && input.useroverride !== "system")
1619
- return { ...darklighttokensof(input.useroverride), source: "user" };
1620
- return { ...darklighttokensof(input.ospreference), source: "os" };
1621
- }
1622
-
1623
- /** Applies one token set to a surface document: every custom property of the theme lands on the root element so every surface, including the dashboardpage, renders with the same tokens. */
1624
- export function applytheme(
1625
- documentroot: { style: { setProperty(name: string, value: string): void } },
1626
- tokens: darklighttokens,
1627
- ): void {
1628
- for (const [name, value] of Object.entries(tokens.tokens)) documentroot.style.setProperty(`--theme-${name}`, value);
1629
- documentroot.style.setProperty("color-scheme", tokens.mode);
1630
- }
1631
-
1632
- /** The high contrast token set of the 2.0.2 final polish (roadmap rc.2 item 35): the same token names the darklight family resolves, retuned so every reading token reaches at least 4.5:1 against its surface and the accent and border reach at least 3:1 under the WCAG relative luminance arithmetic — pure black surfaces with white ink and whitened signal colors in the dark mode, a white surface with black ink and deepened signal colors in the light mode — while the default darklight sets stay untouched beside it. */
1633
- export function highcontrasttokens(mode: darklighttokens["mode"]): darklighttokens {
1634
- const tokens: Record<string, string> =
1635
- mode === "dark"
1636
- ? {
1637
- surface: "#000000",
1638
- elevated: "#000000",
1639
- text: "#ffffff",
1640
- muted: "#e6e6e6",
1641
- accent: "#74e8c1",
1642
- border: "#ffffff",
1643
- focus: "#00e0ff",
1644
- error: "#ffb3c1",
1645
- success: "#b9f6ca",
1646
- warning: "#ffe08a",
1647
- }
1648
- : {
1649
- surface: "#ffffff",
1650
- elevated: "#ffffff",
1651
- text: "#000000",
1652
- muted: "#262626",
1653
- accent: "#2b7ff0",
1654
- border: "#767676",
1655
- focus: "#174ea6",
1656
- error: "#b3261e",
1657
- success: "#0b6b2e",
1658
- warning: "#8a5300",
1659
- };
1660
- return { mode, tokens };
1661
- }
1662
-
1663
- /** Resolves the effective token set under the contrast preference of the 2.0.2 final polish: the default preference keeps the darklight tokens exactly as the family resolved them while the high preference swaps in the high contrast set of the same mode, so the preference never moves the dark or light mode itself. */
1664
- export function contrasttokensof(input: {
1665
- mode: darklighttokens["mode"];
1666
- contrast: "default" | "high";
1667
- }): darklighttokens {
1668
- if (input.contrast === "high") return highcontrasttokens(input.mode);
1669
- return darklighttokensof(input.mode);
1670
- }
1671
-
1672
- /** Applies the resolved tokens with their contrast variant to one surface document: the --theme-* custom properties land on the document root the darklight family already writes while the body carries its data-contrast attribute, so a high contrast surface renders from the same rules and names its variant for the assistive surface. */
1673
- export function applycontrasttheme(
1674
- documentroot: { style: { setProperty(name: string, value: string): void } },
1675
- body: { setAttribute(name: string, value: string): void },
1676
- tokens: darklighttokens,
1677
- contrast: "default" | "high",
1678
- ): void {
1679
- for (const [name, value] of Object.entries(tokens.tokens)) documentroot.style.setProperty(`--theme-${name}`, value);
1680
- documentroot.style.setProperty("color-scheme", tokens.mode);
1681
- body.setAttribute("data-contrast", contrast);
1682
- }
1683
-
1684
- /** The dashboard panel width bounds of the 2.0.2 final polish (roadmap rc.2 item 37): every resizable panel column keeps at least the readable minimum while no column passes the shared maximum, and a known viewport keeps the sibling column its minimum too. */
1685
- export const panelwidthbounds: Readonly<{ minimum: number; maximum: number }> = Object.freeze({
1686
- minimum: 280,
1687
- maximum: 720,
1688
- });
1689
-
1690
- /** Clamps one dashboard panel width inside the bounds: the floor keeps the column readable, the ceiling holds the shared maximum, and a viewport the caller supplies narrows the ceiling so the sibling column never loses its own minimum. */
1691
- export function clamppanelwidth(width: number, viewport?: number | undefined): number {
1692
- if (!Number.isFinite(width)) return panelwidthbounds.minimum;
1693
- const ceiling =
1694
- viewport !== undefined && Number.isFinite(viewport)
1695
- ? Math.min(panelwidthbounds.maximum, Math.max(viewport - panelwidthbounds.minimum, panelwidthbounds.minimum))
1696
- : panelwidthbounds.maximum;
1697
- return Math.min(Math.max(Math.round(width), panelwidthbounds.minimum), ceiling);
1698
- }
1699
-
1700
- /** Parses the persisted panelwidths preferences of the dashboard columns from the layout seam shape: the "left=480" row resolves into the clamped left width while an absent or malformed row keeps the shipped default split. */
1701
- export function panelwidthsof(raw: string | undefined, viewport?: number | undefined): { left: number } {
1702
- if (raw === undefined) return { left: clamppanelwidth(480, viewport) };
1703
- const match = /^left=(\d+)$/.exec(raw.trim());
1704
- if (match === null) return { left: clamppanelwidth(480, viewport) };
1705
- return { left: clamppanelwidth(Number(match[1]), viewport) };
1706
- }
1707
-
1708
- /** Serializes the panelwidths preferences into the layout seam shape: the "left=480" row the per surface preferences persist, one clamped width per named column. */
1709
- export function panelwidthslayout(widths: { left: number }): string {
1710
- return `left=${clamppanelwidth(widths.left)}`;
1711
- }
1712
-
1713
- /** Applies one resize move to the panelwidths state: the pointer delta moves the left column width by its pixels, the clamp holds the bounds and the answer names whether the move clamped, so the drag stops pushing the column past its readable edge. */
1714
- export function resizepanel(input: { widths: { left: number }; delta: number; viewport?: number | undefined }): {
1715
- widths: { left: number };
1716
- clamped: boolean;
1717
- } {
1718
- const target = input.widths.left + input.delta;
1719
- const left = clamppanelwidth(target, input.viewport);
1720
- return { widths: { left }, clamped: left !== Math.round(target) };
1721
- }
1722
-
1723
- /** The shipped locale bundles of the supported languages: every bundle carries the interface strings of its language with english as the fallback base. */
1724
- export function localebundles(): localebundle[] {
1725
- return [
1726
- {
1727
- language: "en",
1728
- strings: {
1729
- "popup.title": "Devthink",
1730
- "popup.taskinput.placeholder": "Describe the goal for the active tab",
1731
- "popup.taskinput.submit": "Propose the plan",
1732
- "popup.palette.open": "Open the commandpalette",
1733
- "popup.recent.title": "Recent runs",
1734
- "popup.recent.resume": "Resume",
1735
- "popup.recent.reopen": "Reopen",
1736
- "sidepanel.tab.plan": "Plan",
1737
- "sidepanel.tab.run": "Run",
1738
- "sidepanel.tab.review": "Review",
1739
- "sidepanel.data.export": "Export",
1740
- "dashboard.title": "Dashboard",
1741
- "options.title": "Options",
1742
- "options.theme.label": "Theme",
1743
- "options.theme.dark": "Dark",
1744
- "options.theme.light": "Light",
1745
- "options.theme.system": "Follow the system",
1746
- "options.locale.label": "Language",
1747
- "options.shortcuts.label": "Shortcutkeys",
1748
- "options.notifications.label": "Notifications",
1749
- "options.importexport.label": "Import and export",
1750
- "options.tour.label": "Feature tour",
1751
- "stepapprove.approve": "Approve",
1752
- "stepapprove.reject": "Reject",
1753
- "stepapprove.edit": "Edit",
1754
- "pagechip.approve": "Approve",
1755
- "pagechip.reject": "Reject",
1756
- "grid.empty": "No extracted rows yet",
1757
- "toast.stepdone": "Step completed",
1758
- },
1759
- },
1760
- {
1761
- language: "pt",
1762
- strings: {
1763
- "popup.title": "Devthink",
1764
- "popup.taskinput.placeholder": "Descreva o objetivo para a aba ativa",
1765
- "popup.taskinput.submit": "Propor o plano",
1766
- "popup.palette.open": "Abrir a paleta de comandos",
1767
- "popup.recent.title": "Execuções recentes",
1768
- "popup.recent.resume": "Retomar",
1769
- "popup.recent.reopen": "Reabrir",
1770
- "sidepanel.tab.plan": "Plano",
1771
- "sidepanel.tab.run": "Execução",
1772
- "sidepanel.tab.review": "Revisão",
1773
- "sidepanel.data.export": "Exportar",
1774
- "dashboard.title": "Painel",
1775
- "options.title": "Opções",
1776
- "options.theme.label": "Tema",
1777
- "options.theme.dark": "Escuro",
1778
- "options.theme.light": "Claro",
1779
- "options.theme.system": "Seguir o sistema",
1780
- "options.locale.label": "Idioma",
1781
- "options.shortcuts.label": "Atalhos",
1782
- "options.notifications.label": "Notificações",
1783
- "options.importexport.label": "Importar e exportar",
1784
- "options.tour.label": "Tour de recursos",
1785
- "stepapprove.approve": "Aprovar",
1786
- "stepapprove.reject": "Rejeitar",
1787
- "stepapprove.edit": "Editar",
1788
- "pagechip.approve": "Aprovar",
1789
- "pagechip.reject": "Rejeitar",
1790
- "grid.empty": "Nenhuma linha extraída ainda",
1791
- "toast.stepdone": "Etapa concluída",
1792
- },
1793
- },
1794
- ];
1795
- }
1796
-
1797
- /** Resolves one interface string in the requested language: a missing string falls back to the english bundle so no control ever renders an empty label. */
1798
- export function localestring(bundles: localebundle[], language: string, key: string): string {
1799
- const requested = bundles.find((bundle) => bundle.language === language);
1800
- const english = bundles.find((bundle) => bundle.language === "en");
1801
- return requested?.strings[key] ?? english?.strings[key] ?? key;
1802
- }
1803
-
1804
- /** The languages the shipped bundles cover. */
1805
- export function supportedlanguages(bundles: localebundle[]): string[] {
1806
- return bundles.map((bundle) => bundle.language);
1807
- }
1808
-
1809
- /** Formats one date, number or duration value per language: dates render with the locale order, numbers with the locale separators and durations as minutes and seconds. */
1810
- export function localeformat(input: { language: string; value: number; kind: "date" | "number" | "duration" }): string {
1811
- if (input.kind === "date") {
1812
- const date = new Date(input.value);
1813
- const year = date.getUTCFullYear();
1814
- const month = String(date.getUTCMonth() + 1).padStart(2, "0");
1815
- const day = String(date.getUTCDate()).padStart(2, "0");
1816
- const hours = String(date.getUTCHours()).padStart(2, "0");
1817
- const minutes = String(date.getUTCMinutes()).padStart(2, "0");
1818
- return input.language === "pt"
1819
- ? `${day}/${month}/${year} ${hours}:${minutes}`
1820
- : `${year}-${month}-${day} ${hours}:${minutes}`;
1821
- }
1822
- if (input.kind === "duration") {
1823
- const seconds = Math.round(input.value / 1000);
1824
- const minutes = Math.floor(seconds / 60);
1825
- const rest = seconds % 60;
1826
- return input.language === "pt" ? `${minutes} min ${rest} s` : `${minutes}m ${rest}s`;
1827
- }
1828
- const text = String(input.value);
1829
- const parts = text.split(".");
1830
- const whole = parts[0] ?? "0";
1831
- const fraction = parts[1];
1832
- const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, input.language === "pt" ? "." : ",");
1833
- return fraction !== undefined ? `${grouped}${input.language === "pt" ? "," : "."}${fraction}` : grouped;
1834
- }
1835
-
1836
- /* ── Merged from quickactions.ts ── */
1837
- import { quickactiongate } from "./policy.js";
1838
-
1839
- /**
1840
- * Quickactions, shortcutkeys and omniboxtask logic of the 1.1.65 family.
1841
- * The path to a run shortens here: quickactions bind the context menu entries of the active tab (extract page, capture shot, run recent and open dashboardpage) behind the origin allowlist of the clicked tab so only permitted actions surface; shortcutkeys bind run, pause, resume, cancelrun and the commandpalette to user editable key combinations that route through the same commandpalette gates; and the omniboxtask parses the keyword text into a taskinput submission that lands in the standard proposal flow with its review intact.
1842
- * No binding is ever forced: every shortcut stays user editable in the optionspage, and no quickaction, shortcut or omnibox entry ever bypasses the human review.
1843
- */
1844
-
1845
- /** The quickaction catalog bound to the context menu entries of the active tab: extract page, capture shot, run recent and open dashboardpage. */
1846
- export function quickactioncatalog(): quickaction[] {
1847
- return [
1848
- { id: "extractpage", label: "Extract page data", command: "starttask", surface: "sidepanel", session: true },
1849
- {
1850
- id: "captureshot",
1851
- label: "Capture a shot",
1852
- command: "starttask",
1853
- surface: "sidepanel",
1854
- permission: "downloads",
1855
- session: true,
1856
- },
1857
- { id: "runrecent", label: "Run the recent task", command: "starttask", surface: "popup", session: true },
1858
- { id: "opendashboardpage", label: "Open the dashboard", command: "opendashboardpage", surface: "dashboardpage" },
1859
- ];
1860
- }
1861
-
1862
- /** Lists only the quickactions the active origin permits: every entry rides the origin allowlist of the clicked tab before it registers. */
1863
- export function quickactionsfor(
1864
- catalog: quickaction[],
1865
- input: { origin: string; granted: string[]; sessionactive: boolean; grantedcapabilities?: string[] },
1866
- ): quickaction[] {
1867
- const capabilities = input.grantedcapabilities ?? ["activeTab", "storage", "scripting", "sidePanel"];
1868
- const grantedcapabilities = capabilities;
1869
- return catalog.filter(
1870
- (action) =>
1871
- quickactiongate({
1872
- action: {
1873
- command: action.command,
1874
- origin: input.origin,
1875
- ...(action.permission !== undefined ? { permission: action.permission } : {}),
1876
- ...(action.session !== undefined ? { session: action.session } : {}),
1877
- },
1878
- granted: input.granted,
1879
- sessionactive: input.sessionactive,
1880
- capabilities: grantedcapabilities,
1881
- }).allowed,
1882
- );
1883
- }
1884
-
1885
- /** The default shortcutkeys bindings: run, pause, resume, cancelrun and the commandpalette open from every surface; every binding stays user editable. */
1886
- export function shortcutdefaults(): shortcutbinding[] {
1887
- return [
1888
- { command: "starttask", key: "Enter", modifiers: [], editable: true, surface: "popup" },
1889
- { command: "pauserun", key: "p", modifiers: ["ctrl", "shift"], editable: true, surface: "sidepanel" },
1890
- { command: "resumerun", key: "r", modifiers: ["ctrl", "shift"], editable: true, surface: "sidepanel" },
1891
- { command: "cancelrun", key: "x", modifiers: ["ctrl", "shift"], editable: true, surface: "sidepanel" },
1892
- { command: "commandpalette", key: ".", modifiers: ["ctrl"], editable: true, surface: "popup" },
1893
- ];
1894
- }
1895
-
1896
- /** Parses one shortcut text such as ctrl+. into its key and modifiers: an empty text refuses the binding. */
1897
- export function parseshortcut(text: string): { key: string; modifiers: string[] } {
1898
- const parts = text
1899
- .trim()
1900
- .toLowerCase()
1901
- .split("+")
1902
- .map((part) => part.trim())
1903
- .filter((part) => part !== "");
1904
- if (parts.length === 0) throw new Error("The shortcut binding needs its key.");
1905
- const modifiers = ["ctrl", "alt", "shift", "meta"];
1906
- const key = parts.filter((part) => !modifiers.includes(part))[0];
1907
- if (key === undefined || key === "") throw new Error("The shortcut binding needs its key beside its modifiers.");
1908
- return { key, modifiers: parts.filter((part) => modifiers.includes(part)) };
1909
- }
1910
-
1911
- /** Formats one shortcut binding as its display text with the modifiers first. */
1912
- export function shortcuttext(binding: shortcutbinding): string {
1913
- return [...binding.modifiers, binding.key].join("+");
1914
- }
1915
-
1916
- /** Edits one shortcut binding: the user chosen combination replaces the binding while an unknown command refuses the edit. */
1917
- export function shortcutbindingafter(bindings: shortcutbinding[], command: string, text: string): shortcutbinding[] {
1918
- const existing = bindings.find((binding) => binding.command === command);
1919
- if (existing === undefined) throw new Error(`The shortcutkeys know no ${command} command to edit.`);
1920
- const parsed = parseshortcut(text);
1921
- return bindings.map((binding) =>
1922
- binding.command === command ? { ...binding, key: parsed.key, modifiers: parsed.modifiers } : binding,
1923
- );
1924
- }
1925
-
1926
- /** Matches one pressed key and modifier set against the bindings: the first command whose combination matches wins, and the palette command matches from every surface. */
1927
- export function shortcutcommandof(
1928
- bindings: shortcutbinding[],
1929
- input: { key: string; modifiers: string[]; surface: uisurface },
1930
- ): string | undefined {
1931
- const pressed = [...input.modifiers].map((modifier) => modifier.toLowerCase()).sort();
1932
- return bindings.find(
1933
- (binding) =>
1934
- binding.key.toLowerCase() === input.key.toLowerCase() &&
1935
- [...binding.modifiers].sort().join("+") === pressed.join("+") &&
1936
- (binding.command === "commandpalette" || binding.surface === input.surface),
1937
- )?.command;
1938
- }
1939
-
1940
- /** Confirms one bound command rides the commandpalette catalog with its gates intact: a shortcut may only trigger a command the palette action gate lists for the current capability set and session state. */
1941
- export function shortcutdispatchable(
1942
- command: string,
1943
- entries: paletteentry[],
1944
- input: { granted: string[]; sessionactive: boolean },
1945
- ): boolean {
1946
- const entry = entries.find((candidate) => candidate.action.command === command);
1947
- if (entry === undefined) return command === "commandpalette";
1948
- return paletteactiongate({
1949
- action: {
1950
- command: entry.action.command,
1951
- ...(entry.action.permission !== undefined ? { permission: entry.action.permission } : {}),
1952
- ...(entry.action.session !== undefined ? { session: entry.action.session } : {}),
1953
- },
1954
- granted: input.granted,
1955
- sessionactive: input.sessionactive,
1956
- }).allowed;
1957
- }
1958
-
1959
- /** Parses one omnibox keyword text into an omniboxtask submission: the text after the keyword becomes the natural language goal of the active origin while an empty text refuses the submission. */
1960
- export function parseomniboxtask(input: { text: string; origin: string; at: number }): omniboxtasksubmission {
1961
- const text = input.text.trim();
1962
- if (text === "") throw new Error("The omnibox task needs its natural language goal after the keyword.");
1963
- if (input.origin.trim() === "") throw new Error("The omnibox task needs its active origin scope.");
1964
- return { id: randomid(), text, origin: input.origin.trim(), surface: "omnibox", at: input.at };
1965
- }
1966
-
1967
- /** Lands one omniboxtask submission in the standard proposal flow: the submission becomes a taskinput of the omnibox surface that routes through the same proposal and review gates as the api. */
1968
- export function omniboxtasktotaskinput(submission: omniboxtasksubmission): taskinputsubmission {
1969
- return {
1970
- id: submission.id,
1971
- text: submission.text,
1972
- context: "",
1973
- origin: submission.origin,
1974
- surface: "omnibox",
1975
- at: submission.at,
1976
- };
1977
- }
1978
-
1979
- /* ── Merged from attentionfeed.ts ── */
1980
-
1981
- /**
1982
- * Attentionfeed logic of the 1.1.66 family.
1983
- * Everything that needs a human lands in one feed: gate waits, phishguard blocks, deferrals and failures each collect as one entry with its cause, its run and gate refs and its deep link to the exact waiting surface, repeated causes per run deduplicate while the first occurrence keeps its time, the severity of the cause ranks the feed, and the retention window of the store stays the user's choice.
1984
- * The feed never resolves anything on its own: a deep link opens the waiting surface and the human resolves the cause exactly as before.
1985
- */
1986
-
1987
- /** Reads the severity rank of one attention cause: a gate wait and a phishguard block hold the run for a human, a deferral waits its window and a failure needs the reviewed retry. */
1988
- export function attentionseverityof(cause: attentioncause): "critical" | "warning" | "info" {
1989
- if (cause === "gatewait" || cause === "phishguard") return "critical";
1990
- if (cause === "deferral") return "warning";
1991
- return "info";
1992
- }
1993
-
1994
- /** Builds the deep link of one attention cause: the link opens the exact surface that resolves the cause. */
1995
- export function attentiondeeplinkof(cause: attentioncause, runid: string, gateref?: string): string {
1996
- if (cause === "gatewait")
1997
- return `devthink://gate/${encodeURIComponent(gateref ?? "unknown")}?run=${encodeURIComponent(runid)}`;
1998
- if (cause === "phishguard") return `devthink://phishguard?run=${encodeURIComponent(runid)}`;
1999
- if (cause === "deferral") return `devthink://deferred?run=${encodeURIComponent(runid)}`;
2000
- return `devthink://error?run=${encodeURIComponent(runid)}`;
2001
- }
2002
-
2003
- /** Builds one attention entry from its source: the cause, the run ref, the gate ref when the run waits at a gate, the summary in plain language and the deep link. */
2004
- export function attentionentryof(input: {
2005
- cause: attentioncause;
2006
- runid: string;
2007
- origin: string;
2008
- summary: string;
2009
- gateref?: string;
2010
- at: number;
2011
- }): attentionentry {
2012
- if (input.runid.trim() === "") throw new Error("The attention entry needs its run ref.");
2013
- if (input.summary.trim() === "") throw new Error("The attention entry needs its summary in plain language.");
2014
- return {
2015
- id: `attention:${input.cause}:${input.runid}:${input.gateref ?? "none"}`,
2016
- cause: input.cause,
2017
- severity: attentionseverityof(input.cause),
2018
- runid: input.runid,
2019
- ...(input.gateref !== undefined && input.gateref.trim() !== "" ? { gateref: input.gateref } : {}),
2020
- origin: input.origin,
2021
- summary: input.summary,
2022
- deeplink: attentiondeeplinkof(input.cause, input.runid, input.gateref),
2023
- at: input.at,
2024
- };
2025
- }
2026
-
2027
- /** Collects the attention entries of one pass: the gate waits the progress recorded, the phishguard blocks, the deferred events and the failed steps each feed one entry. */
2028
- export function collectattention(input: {
2029
- gatewaits?: Array<{ runid: string; stepid: string; gateid: string; kind: string; origin: string; waitedms: number }>;
2030
- phishblocks?: Array<{ runid: string; origin: string; matchedorigin: string; reason: string }>;
2031
- deferrals?: Array<{ runid: string; origin: string; reason: string }>;
2032
- failures?: Array<{ runid: string; stepid: string; origin: string; message: string }>;
2033
- now: number;
2034
- }): attentionentry[] {
2035
- const entries: attentionentry[] = [];
2036
- for (const wait of input.gatewaits ?? [])
2037
- entries.push(
2038
- attentionentryof({
2039
- cause: "gatewait",
2040
- runid: wait.runid,
2041
- origin: wait.origin,
2042
- summary: `The ${wait.kind} gate of the step ${wait.stepid} waits ${wait.waitedms} milliseconds for one human action.`,
2043
- gateref: wait.gateid,
2044
- at: input.now,
2045
- }),
2046
- );
2047
- for (const block of input.phishblocks ?? [])
2048
- entries.push(
2049
- attentionentryof({
2050
- cause: "phishguard",
2051
- runid: block.runid,
2052
- origin: block.origin,
2053
- summary: `The phishguard blocked a credential step on ${block.origin} that resembles the granted ${block.matchedorigin}: ${block.reason}`,
2054
- at: input.now,
2055
- }),
2056
- );
2057
- for (const deferral of input.deferrals ?? [])
2058
- entries.push(
2059
- attentionentryof({
2060
- cause: "deferral",
2061
- runid: deferral.runid,
2062
- origin: deferral.origin,
2063
- summary: `A command deferred past its rate window on ${deferral.origin}: ${deferral.reason}`,
2064
- at: input.now,
2065
- }),
2066
- );
2067
- for (const failure of input.failures ?? [])
2068
- entries.push(
2069
- attentionentryof({
2070
- cause: "failure",
2071
- runid: failure.runid,
2072
- origin: failure.origin,
2073
- summary: `The step ${failure.stepid} failed: ${failure.message}`,
2074
- at: input.now,
2075
- }),
2076
- );
2077
- return entries;
2078
- }
2079
-
2080
- /** Deduplicates repeated causes per run: the first occurrence of one cause inside one run keeps its entry and its time while every later occurrence drops. */
2081
- export function dedupeattention(entries: attentionentry[]): attentionentry[] {
2082
- const seen = new Set<string>();
2083
- const kept: attentionentry[] = [];
2084
- for (const entry of [...entries].sort((a, b) => a.at - b.at)) {
2085
- const key = `${entry.cause}:${entry.runid}:${entry.gateref ?? "none"}`;
2086
- if (seen.has(key)) continue;
2087
- seen.add(key);
2088
- kept.push(entry);
2089
- }
2090
- return kept;
2091
- }
2092
-
2093
- /** Ranks the attention entries by cause severity first and time second: critical gate waits and phishguard blocks stand above warnings and failures. */
2094
- export function rankattention(entries: attentionentry[]): attentionentry[] {
2095
- const order: Record<attentionentry["severity"], number> = { critical: 0, warning: 1, info: 2 };
2096
- return [...entries].sort((a, b) => order[a.severity] - order[b.severity] || b.at - a.at);
2097
- }
2098
-
2099
- /** Reads the count of attention entries the statusbadge surfaces. */
2100
- export function attentioncountof(entries: attentionentry[]): number {
2101
- return rankattention(dedupeattention(entries)).length;
2102
- }
2103
-
2104
- /** Dismisses one attention entry by its id: the dismissal only removes the feed row, the waiting cause keeps its own resolution path. */
2105
- export function dismissattention(entries: attentionentry[], id: string): attentionentry[] {
2106
- return entries.filter((entry) => entry.id !== id);
2107
- }
2108
-
2109
- /** Prunes the attention entries past their retention window: an absent window keeps every entry while the pruned ids return for the audit note. */
2110
- export function pruneattention(
2111
- entries: attentionentry[],
2112
- retention: number | undefined,
2113
- now: number,
2114
- ): { kept: attentionentry[]; pruned: string[] } {
2115
- if (retention === undefined) return { kept: entries, pruned: [] };
2116
- const kept = entries.filter((entry) => now - entry.at < retention);
2117
- return { kept, pruned: entries.filter((entry) => now - entry.at >= retention).map((entry) => entry.id) };
2118
- }
2119
-
2120
- /** Renders the attention entries as system notifications: one content free payload per entry with its deep link, so no page content consent is needed. */
2121
- export function attentionnotifications(
2122
- entries: attentionentry[],
2123
- now: number,
2124
- ): Array<{
2125
- id: string;
2126
- kind: "done" | "attention";
2127
- title: string;
2128
- body: string;
2129
- deeplink: string;
2130
- runid?: string;
2131
- stepid?: string;
2132
- content: boolean;
2133
- at: number;
2134
- }> {
2135
- return rankattention(dedupeattention(entries)).map((entry) => ({
2136
- id: `notify:${entry.id}`,
2137
- kind: "attention" as const,
2138
- title:
2139
- entry.cause === "gatewait"
2140
- ? "A gate waits for you"
2141
- : entry.cause === "phishguard"
2142
- ? "The phishguard blocked a step"
2143
- : entry.cause === "deferral"
2144
- ? "A command deferred"
2145
- : "A step failed",
2146
- body: entry.summary,
2147
- deeplink: entry.deeplink,
2148
- runid: entry.runid,
2149
- ...(entry.gateref !== undefined ? { stepid: entry.gateref } : {}),
2150
- content: false,
2151
- at: now,
2152
- }));
2153
- }