@rubytech/create-sitedesk-code 0.1.526 → 0.1.527

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 (190) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +73 -7
  3. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +7 -0
  4. package/payload/platform/plugins/docs/references/admin-ui.md +70 -4
  5. package/payload/platform/plugins/docs/references/internals.md +1 -1
  6. package/payload/platform/plugins/docs/references/platform.md +1 -1
  7. package/payload/platform/plugins/linkedin-extension/extension/README.md +10 -2
  8. package/payload/platform/plugins/linkedin-extension/extension/background/sw.js +37 -4
  9. package/payload/platform/plugins/linkedin-extension/extension/content/profile.js +17 -2
  10. package/payload/platform/plugins/linkedin-extension/extension/content/thread.js +17 -2
  11. package/payload/platform/plugins/linkedin-extension/extension/manifest.json +1 -0
  12. package/payload/platform/plugins/linkedin-extension/extension/options/options.html +1 -0
  13. package/payload/platform/plugins/linkedin-extension/extension/options/options.js +78 -3
  14. package/payload/server/{chunk-WT2PFGY4.js → chunk-KY3BBT67.js} +216 -1
  15. package/payload/server/maxy-edge.js +226 -6
  16. package/payload/server/public/activity.html +5 -5
  17. package/payload/server/public/agents.html +4 -4
  18. package/payload/server/public/assets/{AdminLoginScreens-BgDjueVe.js → AdminLoginScreens-DSt9DYZZ.js} +1 -1
  19. package/payload/server/public/assets/AdminLoginScreens-DSt9DYZZ.js.br +0 -0
  20. package/payload/server/public/assets/AdminLoginScreens-DSt9DYZZ.js.gz +0 -0
  21. package/payload/server/public/assets/{AdminShell-YctTrR-D.js → AdminShell-MM3_yp3N.js} +1 -1
  22. package/payload/server/public/assets/AdminShell-MM3_yp3N.js.br +0 -0
  23. package/payload/server/public/assets/AdminShell-MM3_yp3N.js.gz +0 -0
  24. package/payload/server/public/assets/{activity-DGQt5ys_.js → activity-aVvbaVd4.js} +1 -1
  25. package/payload/server/public/assets/activity-aVvbaVd4.js.br +0 -0
  26. package/payload/server/public/assets/activity-aVvbaVd4.js.gz +0 -0
  27. package/payload/server/public/assets/{admin-DnUxbB3P.js → admin-B0lQnkH-.js} +1 -1
  28. package/payload/server/public/assets/admin-B0lQnkH-.js.br +0 -0
  29. package/payload/server/public/assets/admin-B0lQnkH-.js.gz +0 -0
  30. package/payload/server/public/assets/{agents-Bc2LZjwC.js → agents-CrpVTPRG.js} +1 -1
  31. package/payload/server/public/assets/agents-CrpVTPRG.js.br +0 -0
  32. package/payload/server/public/assets/agents-CrpVTPRG.js.gz +0 -0
  33. package/payload/server/public/assets/browser-CYHUzTop.js +1 -0
  34. package/payload/server/public/assets/browser-CYHUzTop.js.br +0 -0
  35. package/payload/server/public/assets/browser-CYHUzTop.js.gz +0 -0
  36. package/payload/server/public/assets/{calendar-D25EhXbD.js → calendar-4IWQpwYv.js} +1 -1
  37. package/payload/server/public/assets/calendar-4IWQpwYv.js.br +0 -0
  38. package/payload/server/public/assets/calendar-4IWQpwYv.js.gz +0 -0
  39. package/payload/server/public/assets/chat-DmjZTqnV.js +1 -0
  40. package/payload/server/public/assets/chat-DmjZTqnV.js.br +0 -0
  41. package/payload/server/public/assets/chat-DmjZTqnV.js.gz +0 -0
  42. package/payload/server/public/assets/chevron-left-CIl-s3LZ.js +1 -0
  43. package/payload/server/public/assets/chevron-left-CIl-s3LZ.js.br +0 -0
  44. package/payload/server/public/assets/chevron-right-DpAi2BsF.js +1 -0
  45. package/payload/server/public/assets/chevron-right-DpAi2BsF.js.br +0 -0
  46. package/payload/server/public/assets/clock-CGuM9VRx.js +1 -0
  47. package/payload/server/public/assets/clock-CGuM9VRx.js.br +0 -0
  48. package/payload/server/public/assets/data-BpdFiZKH.js +1 -0
  49. package/payload/server/public/assets/data-BpdFiZKH.js.br +2 -0
  50. package/payload/server/public/assets/data-BpdFiZKH.js.gz +0 -0
  51. package/payload/server/public/assets/{file-download-CiGklOfA.js → file-download-DayBHhKS.js} +1 -1
  52. package/payload/server/public/assets/file-download-DayBHhKS.js.br +0 -0
  53. package/payload/server/public/assets/file-download-DayBHhKS.js.gz +0 -0
  54. package/payload/server/public/assets/{file-text-XjJPULAZ.js → file-text-BSyhVH0m.js} +1 -1
  55. package/payload/server/public/assets/file-text-BSyhVH0m.js.br +0 -0
  56. package/payload/server/public/assets/file-text-BSyhVH0m.js.gz +0 -0
  57. package/payload/server/public/assets/{graph-BjHJ0z_6.js → graph-Bjv3Tc3B.js} +1 -1
  58. package/payload/server/public/assets/graph-Bjv3Tc3B.js.br +0 -0
  59. package/payload/server/public/assets/graph-Bjv3Tc3B.js.gz +0 -0
  60. package/payload/server/public/assets/{graph-labels-D-0u37KA.js → graph-labels-DegLKGdx.js} +1 -1
  61. package/payload/server/public/assets/graph-labels-DegLKGdx.js.br +0 -0
  62. package/payload/server/public/assets/graph-labels-DegLKGdx.js.gz +0 -0
  63. package/payload/server/public/assets/{maximize-2-BJ9S-nfS.js → maximize-2-DJFG2EXJ.js} +1 -1
  64. package/payload/server/public/assets/maximize-2-DJFG2EXJ.js.br +0 -0
  65. package/payload/server/public/assets/maximize-2-DJFG2EXJ.js.gz +0 -0
  66. package/payload/server/public/assets/{operator-Cay4rOOK.js → operator-Du9Ve_PR.js} +1 -1
  67. package/payload/server/public/assets/operator-Du9Ve_PR.js.br +0 -0
  68. package/payload/server/public/assets/operator-Du9Ve_PR.js.gz +0 -0
  69. package/payload/server/public/assets/{page-shuh4ES9.js → page-DgHYxG3M.js} +1 -1
  70. package/payload/server/public/assets/page-DgHYxG3M.js.br +0 -0
  71. package/payload/server/public/assets/page-DgHYxG3M.js.gz +0 -0
  72. package/payload/server/public/assets/{page-Cci_j_y7.js → page-qV1ypb6i.js} +5 -5
  73. package/payload/server/public/assets/page-qV1ypb6i.js.br +0 -0
  74. package/payload/server/public/assets/page-qV1ypb6i.js.gz +0 -0
  75. package/payload/server/public/assets/{public-BbJcQGsO.js → public-CNVEM-Bx.js} +1 -1
  76. package/payload/server/public/assets/public-CNVEM-Bx.js.br +0 -0
  77. package/payload/server/public/assets/public-CNVEM-Bx.js.gz +0 -0
  78. package/payload/server/public/assets/{rotate-ccw-jeSVwUAv.js → rotate-ccw-DQCaky8_.js} +1 -1
  79. package/payload/server/public/assets/rotate-ccw-DQCaky8_.js.br +0 -0
  80. package/payload/server/public/assets/rotate-ccw-DQCaky8_.js.gz +0 -0
  81. package/payload/server/public/assets/{routines-D3M1Wh3a.js → routines-DwK5FeLm.js} +1 -1
  82. package/payload/server/public/assets/routines-DwK5FeLm.js.br +0 -0
  83. package/payload/server/public/assets/routines-DwK5FeLm.js.gz +0 -0
  84. package/payload/server/public/assets/{skills-Bhej2ub1.js → skills-CosN3uA4.js} +1 -1
  85. package/payload/server/public/assets/skills-CosN3uA4.js.br +0 -0
  86. package/payload/server/public/assets/skills-CosN3uA4.js.gz +0 -0
  87. package/payload/server/public/assets/tasks-f0QikHmw.js +3 -0
  88. package/payload/server/public/assets/tasks-f0QikHmw.js.br +0 -0
  89. package/payload/server/public/assets/tasks-f0QikHmw.js.gz +0 -0
  90. package/payload/server/public/assets/{time-entry-format-r5UY9rjG.js → time-entry-format-DLTdS35_.js} +1 -1
  91. package/payload/server/public/assets/time-entry-format-DLTdS35_.js.br +0 -0
  92. package/payload/server/public/assets/time-entry-format-DLTdS35_.js.gz +0 -0
  93. package/payload/server/public/assets/{triangle-alert--PCKaewD.js → triangle-alert-CpwrbdYq.js} +1 -1
  94. package/payload/server/public/assets/triangle-alert-CpwrbdYq.js.br +3 -0
  95. package/payload/server/public/assets/triangle-alert-CpwrbdYq.js.gz +0 -0
  96. package/payload/server/public/assets/{useCopyFeedback-Dg5lNgqH.js → useCopyFeedback-D01gSp36.js} +1 -1
  97. package/payload/server/public/assets/useCopyFeedback-D01gSp36.js.br +0 -0
  98. package/payload/server/public/assets/useCopyFeedback-D01gSp36.js.gz +0 -0
  99. package/payload/server/public/assets/useSubAccountSwitcher-CJzJ9xV3.css +1 -0
  100. package/payload/server/public/assets/useSubAccountSwitcher-CJzJ9xV3.css.br +0 -0
  101. package/payload/server/public/assets/useSubAccountSwitcher-CJzJ9xV3.css.gz +0 -0
  102. package/payload/server/public/assets/{useVoiceRecorder-BY6ZsGMH.js → useVoiceRecorder-DiPgyDUy.js} +1 -1
  103. package/payload/server/public/assets/useVoiceRecorder-DiPgyDUy.js.br +0 -0
  104. package/payload/server/public/assets/useVoiceRecorder-DiPgyDUy.js.gz +0 -0
  105. package/payload/server/public/assets/{wrench-Bw1FMolk.js → wrench-C5SJreEu.js} +1 -1
  106. package/payload/server/public/assets/wrench-C5SJreEu.js.br +1 -0
  107. package/payload/server/public/assets/wrench-C5SJreEu.js.gz +0 -0
  108. package/payload/server/public/brand-defaults.css +1 -1
  109. package/payload/server/public/browser.html +4 -4
  110. package/payload/server/public/calendar.html +7 -7
  111. package/payload/server/public/chat.html +13 -13
  112. package/payload/server/public/data.html +12 -12
  113. package/payload/server/public/graph.html +9 -9
  114. package/payload/server/public/index.html +15 -15
  115. package/payload/server/public/operator.html +14 -14
  116. package/payload/server/public/public.html +13 -13
  117. package/payload/server/public/routines.html +7 -7
  118. package/payload/server/public/skills.html +5 -5
  119. package/payload/server/public/tasks.html +6 -6
  120. package/payload/server/server.js +43 -5
  121. package/payload/server/public/assets/AdminLoginScreens-BgDjueVe.js.br +0 -0
  122. package/payload/server/public/assets/AdminLoginScreens-BgDjueVe.js.gz +0 -0
  123. package/payload/server/public/assets/AdminShell-YctTrR-D.js.br +0 -0
  124. package/payload/server/public/assets/AdminShell-YctTrR-D.js.gz +0 -0
  125. package/payload/server/public/assets/activity-DGQt5ys_.js.br +0 -0
  126. package/payload/server/public/assets/activity-DGQt5ys_.js.gz +0 -0
  127. package/payload/server/public/assets/admin-DnUxbB3P.js.br +0 -0
  128. package/payload/server/public/assets/admin-DnUxbB3P.js.gz +0 -0
  129. package/payload/server/public/assets/agents-Bc2LZjwC.js.br +0 -0
  130. package/payload/server/public/assets/agents-Bc2LZjwC.js.gz +0 -0
  131. package/payload/server/public/assets/browser-BtF7SzvG.js +0 -1
  132. package/payload/server/public/assets/browser-BtF7SzvG.js.br +0 -0
  133. package/payload/server/public/assets/browser-BtF7SzvG.js.gz +0 -0
  134. package/payload/server/public/assets/calendar-D25EhXbD.js.br +0 -0
  135. package/payload/server/public/assets/calendar-D25EhXbD.js.gz +0 -0
  136. package/payload/server/public/assets/chat-z4jsHFXg.js +0 -1
  137. package/payload/server/public/assets/chat-z4jsHFXg.js.br +0 -0
  138. package/payload/server/public/assets/chat-z4jsHFXg.js.gz +0 -0
  139. package/payload/server/public/assets/chevron-left-BD09SN6R.js +0 -1
  140. package/payload/server/public/assets/chevron-right-ChmWeo7T.js +0 -1
  141. package/payload/server/public/assets/chevron-right-ChmWeo7T.js.br +0 -0
  142. package/payload/server/public/assets/clock-Yqf7Ke7I.js +0 -1
  143. package/payload/server/public/assets/clock-Yqf7Ke7I.js.br +0 -0
  144. package/payload/server/public/assets/clock-Yqf7Ke7I.js.gz +0 -0
  145. package/payload/server/public/assets/data-RmnKj5nC.js +0 -1
  146. package/payload/server/public/assets/data-RmnKj5nC.js.br +0 -2
  147. package/payload/server/public/assets/data-RmnKj5nC.js.gz +0 -0
  148. package/payload/server/public/assets/file-download-CiGklOfA.js.br +0 -0
  149. package/payload/server/public/assets/file-download-CiGklOfA.js.gz +0 -0
  150. package/payload/server/public/assets/file-text-XjJPULAZ.js.br +0 -0
  151. package/payload/server/public/assets/file-text-XjJPULAZ.js.gz +0 -0
  152. package/payload/server/public/assets/graph-BjHJ0z_6.js.br +0 -0
  153. package/payload/server/public/assets/graph-BjHJ0z_6.js.gz +0 -0
  154. package/payload/server/public/assets/graph-labels-D-0u37KA.js.br +0 -0
  155. package/payload/server/public/assets/graph-labels-D-0u37KA.js.gz +0 -0
  156. package/payload/server/public/assets/maximize-2-BJ9S-nfS.js.br +0 -0
  157. package/payload/server/public/assets/maximize-2-BJ9S-nfS.js.gz +0 -0
  158. package/payload/server/public/assets/operator-Cay4rOOK.js.br +0 -0
  159. package/payload/server/public/assets/operator-Cay4rOOK.js.gz +0 -0
  160. package/payload/server/public/assets/page-Cci_j_y7.js.br +0 -0
  161. package/payload/server/public/assets/page-Cci_j_y7.js.gz +0 -0
  162. package/payload/server/public/assets/page-shuh4ES9.js.br +0 -0
  163. package/payload/server/public/assets/page-shuh4ES9.js.gz +0 -0
  164. package/payload/server/public/assets/public-BbJcQGsO.js.br +0 -0
  165. package/payload/server/public/assets/public-BbJcQGsO.js.gz +0 -0
  166. package/payload/server/public/assets/rotate-ccw-jeSVwUAv.js.br +0 -0
  167. package/payload/server/public/assets/rotate-ccw-jeSVwUAv.js.gz +0 -0
  168. package/payload/server/public/assets/routines-D3M1Wh3a.js.br +0 -0
  169. package/payload/server/public/assets/routines-D3M1Wh3a.js.gz +0 -0
  170. package/payload/server/public/assets/skills-Bhej2ub1.js.br +0 -0
  171. package/payload/server/public/assets/skills-Bhej2ub1.js.gz +0 -0
  172. package/payload/server/public/assets/tasks-qY-dtOI2.js +0 -1
  173. package/payload/server/public/assets/tasks-qY-dtOI2.js.br +0 -0
  174. package/payload/server/public/assets/tasks-qY-dtOI2.js.gz +0 -0
  175. package/payload/server/public/assets/time-entry-format-r5UY9rjG.js.br +0 -0
  176. package/payload/server/public/assets/time-entry-format-r5UY9rjG.js.gz +0 -0
  177. package/payload/server/public/assets/triangle-alert--PCKaewD.js.br +0 -2
  178. package/payload/server/public/assets/triangle-alert--PCKaewD.js.gz +0 -0
  179. package/payload/server/public/assets/useCopyFeedback-Dg5lNgqH.js.br +0 -0
  180. package/payload/server/public/assets/useCopyFeedback-Dg5lNgqH.js.gz +0 -0
  181. package/payload/server/public/assets/useSubAccountSwitcher-BYfe9vj8.css +0 -1
  182. package/payload/server/public/assets/useSubAccountSwitcher-BYfe9vj8.css.br +0 -0
  183. package/payload/server/public/assets/useSubAccountSwitcher-BYfe9vj8.css.gz +0 -0
  184. package/payload/server/public/assets/useVoiceRecorder-BY6ZsGMH.js.br +0 -0
  185. package/payload/server/public/assets/useVoiceRecorder-BY6ZsGMH.js.gz +0 -0
  186. package/payload/server/public/assets/wrench-Bw1FMolk.js.br +0 -0
  187. package/payload/server/public/assets/wrench-Bw1FMolk.js.gz +0 -0
  188. /package/payload/server/public/assets/{useSubAccountSwitcher-CnpeWIYq.js → useSubAccountSwitcher-BRHYXwGO.js} +0 -0
  189. /package/payload/server/public/assets/{useSubAccountSwitcher-CnpeWIYq.js.br → useSubAccountSwitcher-BRHYXwGO.js.br} +0 -0
  190. /package/payload/server/public/assets/{useSubAccountSwitcher-CnpeWIYq.js.gz → useSubAccountSwitcher-BRHYXwGO.js.gz} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-sitedesk-code",
3
- "version": "0.1.526",
3
+ "version": "0.1.527",
4
4
  "description": "Install SiteDesk — automated back office for independent building contractors",
5
5
  "bin": {
6
6
  "create-sitedesk-code": "./dist/index.js"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what SiteDesk ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:15f533911e93c443dda2624cad785b44ee24ca5bcf5b992fbd96932226abd055
4
+ content-hash: sha256:10d4f123ccb98ccdd68e27e8d22854fb1d9d5dfd985358ff1afa7f5361b25dfa
5
5
  brand: sitedesk-code
6
6
  product-name: SiteDesk
7
7
  ---
@@ -212,7 +212,7 @@ There is no dashboard, no settings panel, no menus. Everything is done through c
212
212
 
213
213
  The chat input auto-grows as you type — it expands to fit your message and shrinks back when you delete text. You can also drag the resize handle above the input to set a custom height.
214
214
 
215
- The admin interface is a three-pane layout: a sidebar on the left with navigation (Sessions, People, Agents, Projects, Tasks, Artefacts) and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu, holding the surface side-by-side with the conversation so the chat stays live while you work in it. At the very top of the sidebar — above the nav rows — a borderless row holds two controls: a "+ New session" button on the left that spawns a fresh Claude Code session, and a Mode trigger on the right showing the current permission mode (Ask, Accept edits, Plan, or Auto). The sidebar's vertical order is: new-session strip first, then the nav (Sessions, People, Agents, Projects, Tasks, Artefacts), then the sessions list, then the footer. Both controls render as plain text-plus-icon affordances with no surrounding rectangle. The "+ New session" button is a text-width hit target — its clickable area is exactly the icon plus label, not the whole row — and shows no hover fill; the only hover feedback is the pointer cursor. The Mode trigger is pushed flush to the right edge of the row. Clicking the Mode trigger opens a popover downward from the row whose header reads "Mode" and lists the four permission modes with the current selection check-marked. The sidebar's nav rows swap the list view in place: Sessions shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). Each recent session row carries a three-state indicator: three pulsing dots when the session is busy (currently processing a turn), a solid sage dot when it is idle (live PTY waiting for input), and a hollow ring when it is archived (PTY exited, JSONL on disk for audit). The list itself splits into three views via a segmented control above the rows: **Active** shows every live session, **Archived** shows every JSONL on disk whose PTY has exited, and **All** shows both. The view choice persists across reloads. An "Include subagents" toggle inside the Active view surfaces specialist spawns (database-operator, premium-plugin agents, anything spawned with a `--agent` flag) which are hidden by default so the list reflects what you started directly. Each row also carries a small uppercase badge — `admin` for operator-driven sessions, the specialist name (for example `db-op`) for background work — so the source of any row is unambiguous at a glance. The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list, because the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable: type in the document and changes save automatically; specialist agent templates are read-only because they ship with SiteDesk and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood; clicking a second project swaps the focus rather than stacking on top. The sidebar's right edge is drag-resizable on every shell-hosted admin page (Sessions root, Chat, Graph, Browser, and Data): drag the handle to widen or narrow the sidebar, and your chosen width is remembered across reloads and shared across all of them. AdminShell mounts the drag handle itself, so every route that wraps its content in `<AdminShell>` gets sidebar resize for free; the handle does not render in the mobile drawer. The chat and artefact divider is also drag-resizable: drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat and artefact min-width floors. Your chosen width is remembered across reloads. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On every viewport the chat header reads left to right as a triptych: a dedicated sidebar toggle (the panel-right icon, which swaps to panel-right-open when the sidebar is showing), the brand mark next to the title in the centre, and the menu burger on the right. This header toggle is the sole sidebar-toggle button; the sidebar itself no longer carries a duplicate. The burger opens the navigation flyout: Dashboard, Data, Graph, Browser, and Chat at the top — Dashboard routes back to `/`, so every satellite page keeps a one-click way home, and the Chat item carries the Claude mark rather than a generic icon — then Public (the agents list) between the two dividers, and below the second divider the version row, Disconnect Claude, and Log out. Tap the sidebar toggle to show or hide the sidebar: on phones (<720px) it slides the drawer in or out, on wider screens it collapses or expands the sidebar column. The brand mark in the centre is decorative; clicks go through the dedicated toggle so the affordance is unambiguous. The drawer animation only fires on tap (220ms slide in or out); resizing your window across the 720px boundary snaps the layout without animation, so you never see a half-open flash. At ≤640px the session metadata pane stacks each label above its value instead of the desktop two-column grid, and the row of action buttons (Open in new tab / Download JSONL / View JSONL / Rename / Pin / Archive / End or Purge) collapses behind a single Actions trigger that opens a popover upward from the foot of the pane. Breakpoint summary: >1280px = full sidebar + chat + artefact pane (drag-resizable divider); 1280px→1080px = sidebar narrows; 1080px→820px = artefact pane hides (Browser/Data/Graph open as full-window pages instead); 820px→720px = sidebar collapses to 56px icon rail; ≤720px = sidebar becomes off-canvas drawer (vertical stack of nav, recents list, foot, the same shape as the desktop sidebar, just on top of the chat instead of beside it).
215
+ The admin interface is a three-pane layout: a sidebar on the left with navigation (Sessions, People, Agents, Projects, Tasks, Artefacts) and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu, holding the surface side-by-side with the conversation so the chat stays live while you work in it. At the very top of the sidebar — above the nav rows — a borderless row holds two controls: a "+ New session" button on the left that spawns a fresh Claude Code session, and a Mode trigger on the right showing the current permission mode (Ask, Accept edits, Plan, or Auto). The sidebar's vertical order is: new-session strip first, then the nav (Sessions, People, Agents, Projects, Tasks, Artefacts), then the sessions list, then the footer. Both controls render as plain text-plus-icon affordances with no surrounding rectangle. The "+ New session" button is a text-width hit target — its clickable area is exactly the icon plus label, not the whole row — and shows no hover fill; the only hover feedback is the pointer cursor. The Mode trigger is pushed flush to the right edge of the row. Clicking the Mode trigger opens a popover downward from the row whose header reads "Mode" and lists the four permission modes with the current selection check-marked. The sidebar's nav rows swap the list view in place: Sessions shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). Each recent session row carries a three-state indicator: three pulsing dots when the session is busy (currently processing a turn), a solid sage dot when it is idle (live PTY waiting for input), and a hollow ring when it is archived (PTY exited, JSONL on disk for audit). The list itself splits into three views via a segmented control above the rows: **Active** shows every live session, **Archived** shows every JSONL on disk whose PTY has exited, and **All** shows both. The view choice persists across reloads. An "Include subagents" toggle inside the Active view surfaces specialist spawns (database-operator, premium-plugin agents, anything spawned with a `--agent` flag) which are hidden by default so the list reflects what you started directly. Each row also carries a small uppercase badge — `admin` for operator-driven sessions, the specialist name (for example `db-op`) for background work — so the source of any row is unambiguous at a glance. The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list, because the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable: type in the document and changes save automatically; specialist agent templates are read-only because they ship with SiteDesk and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood; clicking a second project swaps the focus rather than stacking on top. The sidebar's right edge is drag-resizable on every shell-hosted admin page (Sessions root, Chat, Graph, Browser, and Data): drag the handle to widen or narrow the sidebar, and your chosen width is remembered across reloads and shared across all of them. AdminShell mounts the drag handle itself, so every route that wraps its content in `<AdminShell>` gets sidebar resize for free; the handle does not render in the mobile drawer. The chat and artefact divider is also drag-resizable: drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat and artefact min-width floors. Your chosen width is remembered across reloads. The conversation column on the Chat page has its own width control: a small handle sits half way up the page just inside each side of the column, and dragging either one widens or narrows the reading measure. The column stays centred, so both handles do the same thing — dragging the left one out and the right one out by the same distance give the same width. It will not go below its default 760px and will not go wider than the chat panel itself; the messages, the input box and the jump-to-latest button all move together. Double-click either handle to go back to 760px. Your chosen width is remembered across reloads. The handles do not appear on a phone (below 640px), where the conversation already fills the width. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On every viewport the chat header reads left to right as a triptych: a dedicated sidebar toggle (the panel-right icon, which swaps to panel-right-open when the sidebar is showing), the brand mark next to the title in the centre, and the menu burger on the right. This header toggle is the sole sidebar-toggle button; the sidebar itself no longer carries a duplicate. The burger opens the navigation flyout: Dashboard, Data, Graph, Browser, and Chat at the top — Dashboard routes back to `/`, so every satellite page keeps a one-click way home, and the Chat item carries the Claude mark rather than a generic icon — then Public (the agents list) between the two dividers, and below the second divider the version row, Disconnect Claude, and Log out. Tap the sidebar toggle to show or hide the sidebar: on phones (<720px) it slides the drawer in or out, on wider screens it collapses or expands the sidebar column. The brand mark in the centre is decorative; clicks go through the dedicated toggle so the affordance is unambiguous. The drawer animation only fires on tap (220ms slide in or out); resizing your window across the 720px boundary snaps the layout without animation, so you never see a half-open flash. At ≤640px the session metadata pane stacks each label above its value instead of the desktop two-column grid, and the row of action buttons (Open in new tab / Download JSONL / View JSONL / Rename / Pin / Archive / End or Purge) collapses behind a single Actions trigger that opens a popover upward from the foot of the pane. Breakpoint summary: >1280px = full sidebar + chat + artefact pane (drag-resizable divider); 1280px→1080px = sidebar narrows; 1080px→820px = artefact pane hides (Browser/Data/Graph open as full-window pages instead); 820px→720px = sidebar collapses to 56px icon rail; ≤720px = sidebar becomes off-canvas drawer (vertical stack of nav, recents list, foot, the same shape as the desktop sidebar, just on top of the chat instead of beside it).
216
216
 
217
217
  Page titles are brand-aware: the browser tab shows your product name (e.g. `Real Agent` instead of `SiteDesk`) on every shell — chat, graph, and data — so a non-default brand never leaks the default name in tab strips or browser history.
218
218
 
@@ -4009,15 +4009,58 @@ button** while it holds text — clicking empties the input, clears results
4009
4009
  | Mount | Purpose |
4010
4010
  |---|---|
4011
4011
  | `/browser` | `POST /launch` — launches Chromium on the resolved transport (native display or VNC) on demand. Backs the standalone `/browser` operator page. Lifeline tag `[admin/browser/launch]`. |
4012
+ | `/browser` | `GET /screencast-targets` — the page-target list behind the screencast viewer. Server-side hop to the CDP HTTP surface, which is loopback-only; filtered to `type:'page'`. `501` off the Linux display stack, `502` when Chromium is unreachable. |
4013
+ | `/browser` | `POST /screencast-event` — relay for the client half of the `[browser/screencast]` lifeline. Accepts a fixed op allowlist (`request`, `screencast-started`, `frame`, `input`, `teardown`); the server-side ops are refused so a client cannot emit a line that reads as a server decision. |
4012
4014
 
4013
4015
  The in-chat / artefact-embedded brand-VNC iframe components and the
4014
4016
  `/browser-iframe` event-beacon route were retired and stay
4015
4017
  deleted; the `/device-browser` (navigate-the-device-tab) route was also
4016
4018
  removed and is not restored. The standalone `/browser` page is a
4017
- sanctioned operator surface peer to `/graph` and `/data`: it embeds the live
4018
- VNC viewer (`/vnc-viewer.html`, popout `/vnc-popout.html`) and calls
4019
- `/api/admin/browser/launch`. On darwin (no noVNC asset) it shows a "VNC surface
4020
- not available" state.
4019
+ sanctioned operator surface peer to `/graph` and `/data`.
4020
+
4021
+ **The human view is a CDP screencast, not a framebuffer.** `/browser`
4022
+ renders `CdpScreencastSurface`: frames arrive as CDP `Page.screencastFrame` and
4023
+ paint to a canvas, and the operator's clicks, scrolling and typing leave as
4024
+ `Input.dispatch*`. Interaction therefore lands on page coordinates rather than
4025
+ screen pixels, and the view is one tab chosen from a strip rather than the whole
4026
+ X display. `VncSurface` and `/vnc-viewer.html` still exist and still work, but
4027
+ nothing on `/browser` reaches them, and retiring that stack is separate work.
4028
+
4029
+ The transport is `/cdp-screencast` on the dashboard's own host and port, claimed
4030
+ by the edge listener before its `forwardUpgrade` catch-all. It is gated by
4031
+ `canAccessAdmin` and rejected on operator and public hosts (`reason` values
4032
+ `origin-operator-host` and `origin-public-host`, mirroring the websockify
4033
+ rejection), and it validates the target id against a fixed 32-hex charset because
4034
+ that id is interpolated into the upstream request line. Before replaying the
4035
+ upgrade to Chromium it drops the admin session cookie and the browser `Origin`:
4036
+ Chromium is launched with no `--remote-allow-origins`, so its DevTools endpoint
4037
+ has no allowlisted origin, and the proxy has already checked same-origin itself.
4038
+
4039
+ Chromium's debugging port is bound to `127.0.0.1` on `${CDP_PORT}` and is never
4040
+ tunnelled or bound off-localhost. That port carries no authentication and total
4041
+ control of the browser, which is why the proxy is the only path to it. Per-brand
4042
+ isolation is the `CDP_PORT` import and nothing else, so two co-resident brands
4043
+ each view their own Chromium.
4044
+
4045
+ Off the Linux display stack, and whenever Chromium is unreachable, the surface
4046
+ renders an explicit unavailable state. That is deliberate rather than cosmetic: a
4047
+ canvas that never receives a frame is indistinguishable from a black desktop,
4048
+ where the iframe it replaced failed visibly.
4049
+
4050
+ Observability is `[browser/screencast]` correlated by `sid` across
4051
+ `request → gate → cdp-connected → screencast-started → frame → input → teardown
4052
+ → socket-closed`, plus `[cdp-ws-census] liveSockets=<N> sessions=<M>` on every
4053
+ open and close. Diagnostic path:
4054
+
4055
+ ```
4056
+ logs-read.sh | grep -E '\[browser/screencast\]|\[cdp-ws-census\]'
4057
+ ```
4058
+
4059
+ then filter to one `sid` for a whole session. Frame lines are sampled, so a
4060
+ frozen `seq` against a live socket is the frozen-viewer signature. `liveSockets`
4061
+ climbing while `sessions` stays flat is the leak signature: an operator tab that
4062
+ closes without a clean teardown emits no action log of its own, so the standing
4063
+ reconcile pass closes client-dead sockets and reports `op=orphan-reap closed=<N>`.
4021
4064
 
4022
4065
  ### Diagnostics
4023
4066
 
@@ -4318,6 +4361,29 @@ on every admin page; double-click resets to half the available width
4318
4361
  (viewport minus sidebar), clamped to the per-pane min-width floors. The
4319
4362
  chosen width is remembered across reloads.
4320
4363
 
4364
+ **The /chat reading measure is operator-set.** `--webchat-measure` was a fixed
4365
+ 760px; it is now the FLOOR of a range the operator drags, written inline on
4366
+ `.webchat-page` by `WebchatSplitter` so the message column, the composer and the
4367
+ jump-to-latest button — all three read the same property — stay aligned. Two
4368
+ handles, one just inside each column edge, vertically centred. The column is
4369
+ centre-aligned, so one symmetric rule drives both: `measure = 2 × |pointerX −
4370
+ panelCentreX|`, which is why the two handles are interchangeable. Envelope:
4371
+ 760px ≤ measure ≤ the chat panel's own width; double-click either handle to
4372
+ revert to 760px and clear the stored value; the measure persists in
4373
+ `localStorage` under `maxy-webchat-measure-px` and re-clamps on window resize and
4374
+ on any panel-width change (sidebar drag, artefact open/close). No handle renders
4375
+ below a 640px viewport, where the media query already gives the column the full
4376
+ width. A measure wider than its panel does not overflow — the column just fills
4377
+ the panel — so that state emits nothing on its own; the standing check is
4378
+ `[admin-ui] op=webchat-measure-audit measurePx= panelPx= availPx= columnPx=
4379
+ handles= governed= fit=`, emitted on mount and on every re-clamp with identical
4380
+ consecutive readings suppressed. `columnPx` is the measured `.wa-thread-scroll`
4381
+ rect, not the property read back, so a property written to the wrong element
4382
+ shows as `measurePx=1200 columnPx=760`; `columnPx=-1` means no transcript is
4383
+ mounted (the greeting panel). `fit=false` or `handles=` disagreeing with
4384
+ `governed=` is the failure. A completed drag logs `[admin-ui] webchat-resize
4385
+ px=<final>`; a bare click logs nothing.
4386
+
4321
4387
  ## Health vs version
4322
4388
 
4323
4389
  Two endpoints, two surfaces, two restart-survival roles:
@@ -5315,7 +5381,7 @@ For estate-agent client work surfaced on the `/tasks` operator page, `work-creat
5315
5381
 
5316
5382
  Two surfaces feed the gate. (1) **Workflow path:** `memory-write` accepts an optional `producedByTaskId` parameter. When set, an inbound `:PRODUCED` edge from that Task is composed into the write's relationships before the gate runs — the typical agent-side pattern is to call `work-create` at the start of an autonomous flow, capture `taskId`, and pass it as `producedByTaskId` on every subsequent `memory-write` for a gated label. The gate verifies Task and write share the same `accountId`; mismatch is rejected loud. (2) **Direct-ask path:** the admin server resolves the active `:AdminConversation`'s `sessionId` UUID and stamps it as `SESSION_NODE_ID` in the spawn env at PTY-spawn time. The same stamp propagates onto specialist subagent spawns the admin dispatches so listing-curator, content-producer, database-operator etc. inherit the same conversation anchor. The `contact-create` and `memory-write` wrappers call `injectConversationProvenance` (exported from [`@maxy/graph-write`](../../../lib/graph-write/src/conversation-provenance.ts)) which MATCHes `(c:Conversation {sessionId, accountId})` — account isolation is part of the natural key, not a separate gate — and prepends the synthetic `:PRODUCED` edge (composed by Neo4j elementId, which the helper reads off the MATCH). No agent-visible schema field changes. `memory-write` uses the env-stamp only as a fallback when `producedByTaskId` is unset; `contact-create` has no `producedByTaskId` parameter today and relies on the env-stamp alone. Autonomous (cron-driven) specialists with no parent conversation legitimately have no env-stamp; those must thread `producedByTaskId`.
5317
5383
 
5318
- A task also carries a **workspace** edge, distinct from the two provenance ones above: `(t:Task)-[:WORKED_ON_IN]->(c:Conversation)` is the session an operator works the task in, written only by the `/tasks` page Session control. `RAISED_DURING` and `PART_OF` say where a task was *raised*, so neither can answer "which chat do I continue this task in" — a task raised in an unrelated conversation would resume that one. At most one such edge exists per task: the control's link route deletes any edge to a different Conversation before merging the new one, which is how a resume the session manager redirected to a live fork re-points itself rather than staying on the dead source. The `:Task` also carries `sessionSpawnRequestedAt`, stamped before the spawn and removed when the link lands, so a session that was created but never linked is countable rather than invisible.
5384
+ A task also carries a **workspace** edge, distinct from the two provenance ones above: `(t:Task)-[:WORKED_ON_IN]->(c:Conversation)` is the session an operator works the task in, written only by the `/tasks` page Session control. `RAISED_DURING` and `PART_OF` say where a task was *raised*, so neither can answer "which chat do I continue this task in" — a task raised in an unrelated conversation would resume that one. At most one such edge exists per task: the control's link route deletes any edge to a different Conversation before merging the new one. The link route runs only on a first click, when the task has no session yet; a click on an already-linked task navigates straight to `/chat?session=<id>` and writes nothing, so an edge is never re-pointed after it is first written. The `:Task` also carries `sessionSpawnRequestedAt`, stamped before the spawn and removed when the link lands, so a session that was created but never linked is countable rather than invisible.
5319
5385
 
5320
5386
  Operator audit cyphers:
5321
5387
  - "What entities did this conversation's actions produce?" — `MATCH (c:AdminConversation {sessionId:$id})<-[:RAISED_DURING]-(t:Task)-[:PRODUCED]->(e) RETURN labels(e), e.name, t.kind, t.status`
@@ -9,6 +9,13 @@ Invoked by the admin agent directly.
9
9
 
10
10
  This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
11
11
 
12
+ ## 2026-07-30 (0.1.527)
13
+
14
+ - The remote browser page now shows a live picture of the browser you can click and type into directly, replacing the old screen-sharing view.
15
+ - The Session button on an open task now opens the chat inside the dashboard instead of sending you out to a different site, and the new session starts already knowing the task's description and notes.
16
+ - Posting to LinkedIn from the browser extension works again. It was being stopped before the post ever left the browser, and when it failed it blamed the wrong thing; a missing permission and an unreachable device are now reported as the different problems they are.
17
+ - The page title is smaller and the chat column can be dragged wider or narrower.
18
+
12
19
  ## 2026-07-30 (0.1.526)
13
20
 
14
21
  - Asking WhatsApp to reconnect now answers as soon as the reconnect starts, instead of waiting on the old connection to die and leaving you with no reply at all.
@@ -783,15 +783,58 @@ button** while it holds text — clicking empties the input, clears results
783
783
  | Mount | Purpose |
784
784
  |---|---|
785
785
  | `/browser` | `POST /launch` — launches Chromium on the resolved transport (native display or VNC) on demand. Backs the standalone `/browser` operator page. Lifeline tag `[admin/browser/launch]`. |
786
+ | `/browser` | `GET /screencast-targets` — the page-target list behind the screencast viewer. Server-side hop to the CDP HTTP surface, which is loopback-only; filtered to `type:'page'`. `501` off the Linux display stack, `502` when Chromium is unreachable. |
787
+ | `/browser` | `POST /screencast-event` — relay for the client half of the `[browser/screencast]` lifeline. Accepts a fixed op allowlist (`request`, `screencast-started`, `frame`, `input`, `teardown`); the server-side ops are refused so a client cannot emit a line that reads as a server decision. |
786
788
 
787
789
  The in-chat / artefact-embedded brand-VNC iframe components and the
788
790
  `/browser-iframe` event-beacon route were retired and stay
789
791
  deleted; the `/device-browser` (navigate-the-device-tab) route was also
790
792
  removed and is not restored. The standalone `/browser` page is a
791
- sanctioned operator surface peer to `/graph` and `/data`: it embeds the live
792
- VNC viewer (`/vnc-viewer.html`, popout `/vnc-popout.html`) and calls
793
- `/api/admin/browser/launch`. On darwin (no noVNC asset) it shows a "VNC surface
794
- not available" state.
793
+ sanctioned operator surface peer to `/graph` and `/data`.
794
+
795
+ **The human view is a CDP screencast, not a framebuffer.** `/browser`
796
+ renders `CdpScreencastSurface`: frames arrive as CDP `Page.screencastFrame` and
797
+ paint to a canvas, and the operator's clicks, scrolling and typing leave as
798
+ `Input.dispatch*`. Interaction therefore lands on page coordinates rather than
799
+ screen pixels, and the view is one tab chosen from a strip rather than the whole
800
+ X display. `VncSurface` and `/vnc-viewer.html` still exist and still work, but
801
+ nothing on `/browser` reaches them, and retiring that stack is separate work.
802
+
803
+ The transport is `/cdp-screencast` on the dashboard's own host and port, claimed
804
+ by the edge listener before its `forwardUpgrade` catch-all. It is gated by
805
+ `canAccessAdmin` and rejected on operator and public hosts (`reason` values
806
+ `origin-operator-host` and `origin-public-host`, mirroring the websockify
807
+ rejection), and it validates the target id against a fixed 32-hex charset because
808
+ that id is interpolated into the upstream request line. Before replaying the
809
+ upgrade to Chromium it drops the admin session cookie and the browser `Origin`:
810
+ Chromium is launched with no `--remote-allow-origins`, so its DevTools endpoint
811
+ has no allowlisted origin, and the proxy has already checked same-origin itself.
812
+
813
+ Chromium's debugging port is bound to `127.0.0.1` on `${CDP_PORT}` and is never
814
+ tunnelled or bound off-localhost. That port carries no authentication and total
815
+ control of the browser, which is why the proxy is the only path to it. Per-brand
816
+ isolation is the `CDP_PORT` import and nothing else, so two co-resident brands
817
+ each view their own Chromium.
818
+
819
+ Off the Linux display stack, and whenever Chromium is unreachable, the surface
820
+ renders an explicit unavailable state. That is deliberate rather than cosmetic: a
821
+ canvas that never receives a frame is indistinguishable from a black desktop,
822
+ where the iframe it replaced failed visibly.
823
+
824
+ Observability is `[browser/screencast]` correlated by `sid` across
825
+ `request → gate → cdp-connected → screencast-started → frame → input → teardown
826
+ → socket-closed`, plus `[cdp-ws-census] liveSockets=<N> sessions=<M>` on every
827
+ open and close. Diagnostic path:
828
+
829
+ ```
830
+ logs-read.sh | grep -E '\[browser/screencast\]|\[cdp-ws-census\]'
831
+ ```
832
+
833
+ then filter to one `sid` for a whole session. Frame lines are sampled, so a
834
+ frozen `seq` against a live socket is the frozen-viewer signature. `liveSockets`
835
+ climbing while `sessions` stays flat is the leak signature: an operator tab that
836
+ closes without a clean teardown emits no action log of its own, so the standing
837
+ reconcile pass closes client-dead sockets and reports `op=orphan-reap closed=<N>`.
795
838
 
796
839
  ### Diagnostics
797
840
 
@@ -1092,6 +1135,29 @@ on every admin page; double-click resets to half the available width
1092
1135
  (viewport minus sidebar), clamped to the per-pane min-width floors. The
1093
1136
  chosen width is remembered across reloads.
1094
1137
 
1138
+ **The /chat reading measure is operator-set.** `--webchat-measure` was a fixed
1139
+ 760px; it is now the FLOOR of a range the operator drags, written inline on
1140
+ `.webchat-page` by `WebchatSplitter` so the message column, the composer and the
1141
+ jump-to-latest button — all three read the same property — stay aligned. Two
1142
+ handles, one just inside each column edge, vertically centred. The column is
1143
+ centre-aligned, so one symmetric rule drives both: `measure = 2 × |pointerX −
1144
+ panelCentreX|`, which is why the two handles are interchangeable. Envelope:
1145
+ 760px ≤ measure ≤ the chat panel's own width; double-click either handle to
1146
+ revert to 760px and clear the stored value; the measure persists in
1147
+ `localStorage` under `maxy-webchat-measure-px` and re-clamps on window resize and
1148
+ on any panel-width change (sidebar drag, artefact open/close). No handle renders
1149
+ below a 640px viewport, where the media query already gives the column the full
1150
+ width. A measure wider than its panel does not overflow — the column just fills
1151
+ the panel — so that state emits nothing on its own; the standing check is
1152
+ `[admin-ui] op=webchat-measure-audit measurePx= panelPx= availPx= columnPx=
1153
+ handles= governed= fit=`, emitted on mount and on every re-clamp with identical
1154
+ consecutive readings suppressed. `columnPx` is the measured `.wa-thread-scroll`
1155
+ rect, not the property read back, so a property written to the wrong element
1156
+ shows as `measurePx=1200 columnPx=760`; `columnPx=-1` means no transcript is
1157
+ mounted (the greeting panel). `fit=false` or `handles=` disagreeing with
1158
+ `governed=` is the failure. A completed drag logs `[admin-ui] webchat-resize
1159
+ px=<final>`; a bare click logs nothing.
1160
+
1095
1161
  ## Health vs version
1096
1162
 
1097
1163
  Two endpoints, two surfaces, two restart-survival roles:
@@ -560,7 +560,7 @@ For estate-agent client work surfaced on the `/tasks` operator page, `work-creat
560
560
 
561
561
  Two surfaces feed the gate. (1) **Workflow path:** `memory-write` accepts an optional `producedByTaskId` parameter. When set, an inbound `:PRODUCED` edge from that Task is composed into the write's relationships before the gate runs — the typical agent-side pattern is to call `work-create` at the start of an autonomous flow, capture `taskId`, and pass it as `producedByTaskId` on every subsequent `memory-write` for a gated label. The gate verifies Task and write share the same `accountId`; mismatch is rejected loud. (2) **Direct-ask path:** the admin server resolves the active `:AdminConversation`'s `sessionId` UUID and stamps it as `SESSION_NODE_ID` in the spawn env at PTY-spawn time. The same stamp propagates onto specialist subagent spawns the admin dispatches so listing-curator, content-producer, database-operator etc. inherit the same conversation anchor. The `contact-create` and `memory-write` wrappers call `injectConversationProvenance` (exported from [`@maxy/graph-write`](../../../lib/graph-write/src/conversation-provenance.ts)) which MATCHes `(c:Conversation {sessionId, accountId})` — account isolation is part of the natural key, not a separate gate — and prepends the synthetic `:PRODUCED` edge (composed by Neo4j elementId, which the helper reads off the MATCH). No agent-visible schema field changes. `memory-write` uses the env-stamp only as a fallback when `producedByTaskId` is unset; `contact-create` has no `producedByTaskId` parameter today and relies on the env-stamp alone. Autonomous (cron-driven) specialists with no parent conversation legitimately have no env-stamp; those must thread `producedByTaskId`.
562
562
 
563
- A task also carries a **workspace** edge, distinct from the two provenance ones above: `(t:Task)-[:WORKED_ON_IN]->(c:Conversation)` is the session an operator works the task in, written only by the `/tasks` page Session control. `RAISED_DURING` and `PART_OF` say where a task was *raised*, so neither can answer "which chat do I continue this task in" — a task raised in an unrelated conversation would resume that one. At most one such edge exists per task: the control's link route deletes any edge to a different Conversation before merging the new one, which is how a resume the session manager redirected to a live fork re-points itself rather than staying on the dead source. The `:Task` also carries `sessionSpawnRequestedAt`, stamped before the spawn and removed when the link lands, so a session that was created but never linked is countable rather than invisible.
563
+ A task also carries a **workspace** edge, distinct from the two provenance ones above: `(t:Task)-[:WORKED_ON_IN]->(c:Conversation)` is the session an operator works the task in, written only by the `/tasks` page Session control. `RAISED_DURING` and `PART_OF` say where a task was *raised*, so neither can answer "which chat do I continue this task in" — a task raised in an unrelated conversation would resume that one. At most one such edge exists per task: the control's link route deletes any edge to a different Conversation before merging the new one. The link route runs only on a first click, when the task has no session yet; a click on an already-linked task navigates straight to `/chat?session=<id>` and writes nothing, so an edge is never re-pointed after it is first written. The `:Task` also carries `sessionSpawnRequestedAt`, stamped before the spawn and removed when the link lands, so a session that was created but never linked is countable rather than invisible.
564
564
 
565
565
  Operator audit cyphers:
566
566
  - "What entities did this conversation's actions produce?" — `MATCH (c:AdminConversation {sessionId:$id})<-[:RAISED_DURING]-(t:Task)-[:PRODUCED]->(e) RETURN labels(e), e.name, t.kind, t.status`
@@ -85,7 +85,7 @@ There is no dashboard, no settings panel, no menus. Everything is done through c
85
85
 
86
86
  The chat input auto-grows as you type — it expands to fit your message and shrinks back when you delete text. You can also drag the resize handle above the input to set a custom height.
87
87
 
88
- The admin interface is a three-pane layout: a sidebar on the left with navigation (Sessions, People, Agents, Projects, Tasks, Artefacts) and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu, holding the surface side-by-side with the conversation so the chat stays live while you work in it. At the very top of the sidebar — above the nav rows — a borderless row holds two controls: a "+ New session" button on the left that spawns a fresh Claude Code session, and a Mode trigger on the right showing the current permission mode (Ask, Accept edits, Plan, or Auto). The sidebar's vertical order is: new-session strip first, then the nav (Sessions, People, Agents, Projects, Tasks, Artefacts), then the sessions list, then the footer. Both controls render as plain text-plus-icon affordances with no surrounding rectangle. The "+ New session" button is a text-width hit target — its clickable area is exactly the icon plus label, not the whole row — and shows no hover fill; the only hover feedback is the pointer cursor. The Mode trigger is pushed flush to the right edge of the row. Clicking the Mode trigger opens a popover downward from the row whose header reads "Mode" and lists the four permission modes with the current selection check-marked. The sidebar's nav rows swap the list view in place: Sessions shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). Each recent session row carries a three-state indicator: three pulsing dots when the session is busy (currently processing a turn), a solid sage dot when it is idle (live PTY waiting for input), and a hollow ring when it is archived (PTY exited, JSONL on disk for audit). The list itself splits into three views via a segmented control above the rows: **Active** shows every live session, **Archived** shows every JSONL on disk whose PTY has exited, and **All** shows both. The view choice persists across reloads. An "Include subagents" toggle inside the Active view surfaces specialist spawns (database-operator, premium-plugin agents, anything spawned with a `--agent` flag) which are hidden by default so the list reflects what you started directly. Each row also carries a small uppercase badge — `admin` for operator-driven sessions, the specialist name (for example `db-op`) for background work — so the source of any row is unambiguous at a glance. The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list, because the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable: type in the document and changes save automatically; specialist agent templates are read-only because they ship with SiteDesk and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood; clicking a second project swaps the focus rather than stacking on top. The sidebar's right edge is drag-resizable on every shell-hosted admin page (Sessions root, Chat, Graph, Browser, and Data): drag the handle to widen or narrow the sidebar, and your chosen width is remembered across reloads and shared across all of them. AdminShell mounts the drag handle itself, so every route that wraps its content in `<AdminShell>` gets sidebar resize for free; the handle does not render in the mobile drawer. The chat and artefact divider is also drag-resizable: drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat and artefact min-width floors. Your chosen width is remembered across reloads. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On every viewport the chat header reads left to right as a triptych: a dedicated sidebar toggle (the panel-right icon, which swaps to panel-right-open when the sidebar is showing), the brand mark next to the title in the centre, and the menu burger on the right. This header toggle is the sole sidebar-toggle button; the sidebar itself no longer carries a duplicate. The burger opens the navigation flyout: Dashboard, Data, Graph, Browser, and Chat at the top — Dashboard routes back to `/`, so every satellite page keeps a one-click way home, and the Chat item carries the Claude mark rather than a generic icon — then Public (the agents list) between the two dividers, and below the second divider the version row, Disconnect Claude, and Log out. Tap the sidebar toggle to show or hide the sidebar: on phones (<720px) it slides the drawer in or out, on wider screens it collapses or expands the sidebar column. The brand mark in the centre is decorative; clicks go through the dedicated toggle so the affordance is unambiguous. The drawer animation only fires on tap (220ms slide in or out); resizing your window across the 720px boundary snaps the layout without animation, so you never see a half-open flash. At ≤640px the session metadata pane stacks each label above its value instead of the desktop two-column grid, and the row of action buttons (Open in new tab / Download JSONL / View JSONL / Rename / Pin / Archive / End or Purge) collapses behind a single Actions trigger that opens a popover upward from the foot of the pane. Breakpoint summary: >1280px = full sidebar + chat + artefact pane (drag-resizable divider); 1280px→1080px = sidebar narrows; 1080px→820px = artefact pane hides (Browser/Data/Graph open as full-window pages instead); 820px→720px = sidebar collapses to 56px icon rail; ≤720px = sidebar becomes off-canvas drawer (vertical stack of nav, recents list, foot, the same shape as the desktop sidebar, just on top of the chat instead of beside it).
88
+ The admin interface is a three-pane layout: a sidebar on the left with navigation (Sessions, People, Agents, Projects, Tasks, Artefacts) and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu, holding the surface side-by-side with the conversation so the chat stays live while you work in it. At the very top of the sidebar — above the nav rows — a borderless row holds two controls: a "+ New session" button on the left that spawns a fresh Claude Code session, and a Mode trigger on the right showing the current permission mode (Ask, Accept edits, Plan, or Auto). The sidebar's vertical order is: new-session strip first, then the nav (Sessions, People, Agents, Projects, Tasks, Artefacts), then the sessions list, then the footer. Both controls render as plain text-plus-icon affordances with no surrounding rectangle. The "+ New session" button is a text-width hit target — its clickable area is exactly the icon plus label, not the whole row — and shows no hover fill; the only hover feedback is the pointer cursor. The Mode trigger is pushed flush to the right edge of the row. Clicking the Mode trigger opens a popover downward from the row whose header reads "Mode" and lists the four permission modes with the current selection check-marked. The sidebar's nav rows swap the list view in place: Sessions shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). Each recent session row carries a three-state indicator: three pulsing dots when the session is busy (currently processing a turn), a solid sage dot when it is idle (live PTY waiting for input), and a hollow ring when it is archived (PTY exited, JSONL on disk for audit). The list itself splits into three views via a segmented control above the rows: **Active** shows every live session, **Archived** shows every JSONL on disk whose PTY has exited, and **All** shows both. The view choice persists across reloads. An "Include subagents" toggle inside the Active view surfaces specialist spawns (database-operator, premium-plugin agents, anything spawned with a `--agent` flag) which are hidden by default so the list reflects what you started directly. Each row also carries a small uppercase badge — `admin` for operator-driven sessions, the specialist name (for example `db-op`) for background work — so the source of any row is unambiguous at a glance. The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list, because the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable: type in the document and changes save automatically; specialist agent templates are read-only because they ship with SiteDesk and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood; clicking a second project swaps the focus rather than stacking on top. The sidebar's right edge is drag-resizable on every shell-hosted admin page (Sessions root, Chat, Graph, Browser, and Data): drag the handle to widen or narrow the sidebar, and your chosen width is remembered across reloads and shared across all of them. AdminShell mounts the drag handle itself, so every route that wraps its content in `<AdminShell>` gets sidebar resize for free; the handle does not render in the mobile drawer. The chat and artefact divider is also drag-resizable: drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat and artefact min-width floors. Your chosen width is remembered across reloads. The conversation column on the Chat page has its own width control: a small handle sits half way up the page just inside each side of the column, and dragging either one widens or narrows the reading measure. The column stays centred, so both handles do the same thing — dragging the left one out and the right one out by the same distance give the same width. It will not go below its default 760px and will not go wider than the chat panel itself; the messages, the input box and the jump-to-latest button all move together. Double-click either handle to go back to 760px. Your chosen width is remembered across reloads. The handles do not appear on a phone (below 640px), where the conversation already fills the width. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On every viewport the chat header reads left to right as a triptych: a dedicated sidebar toggle (the panel-right icon, which swaps to panel-right-open when the sidebar is showing), the brand mark next to the title in the centre, and the menu burger on the right. This header toggle is the sole sidebar-toggle button; the sidebar itself no longer carries a duplicate. The burger opens the navigation flyout: Dashboard, Data, Graph, Browser, and Chat at the top — Dashboard routes back to `/`, so every satellite page keeps a one-click way home, and the Chat item carries the Claude mark rather than a generic icon — then Public (the agents list) between the two dividers, and below the second divider the version row, Disconnect Claude, and Log out. Tap the sidebar toggle to show or hide the sidebar: on phones (<720px) it slides the drawer in or out, on wider screens it collapses or expands the sidebar column. The brand mark in the centre is decorative; clicks go through the dedicated toggle so the affordance is unambiguous. The drawer animation only fires on tap (220ms slide in or out); resizing your window across the 720px boundary snaps the layout without animation, so you never see a half-open flash. At ≤640px the session metadata pane stacks each label above its value instead of the desktop two-column grid, and the row of action buttons (Open in new tab / Download JSONL / View JSONL / Rename / Pin / Archive / End or Purge) collapses behind a single Actions trigger that opens a popover upward from the foot of the pane. Breakpoint summary: >1280px = full sidebar + chat + artefact pane (drag-resizable divider); 1280px→1080px = sidebar narrows; 1080px→820px = artefact pane hides (Browser/Data/Graph open as full-window pages instead); 820px→720px = sidebar collapses to 56px icon rail; ≤720px = sidebar becomes off-canvas drawer (vertical stack of nav, recents list, foot, the same shape as the desktop sidebar, just on top of the chat instead of beside it).
89
89
 
90
90
  Page titles are brand-aware: the browser tab shows your product name (e.g. `Real Agent` instead of `SiteDesk`) on every shell — chat, graph, and data — so a non-default brand never leaks the default name in tab strips or browser history.
91
91
 
@@ -9,6 +9,7 @@ Unpacked-extension dev build. No Chrome Web Store publication.
9
9
  3. **Load unpacked** → select this `extension/` directory.
10
10
  4. Click the new puzzle icon → open **SiteDesk LinkedIn Ingest** options.
11
11
  5. Paste your admin host (e.g. `https://your-tunnel.example.com`) and your admin **session key** (the value of `session_key` on the cookie or query in your admin browser). Save.
12
+ 6. Chrome asks you to allow the extension to talk to that host. **Allow it.** The pill cannot post without it, so declining saves nothing.
12
13
 
13
14
  ## Use
14
15
 
@@ -16,7 +17,14 @@ Unpacked-extension dev build. No Chrome Web Store publication.
16
17
  - Click the **Add to SiteDesk** pill at the top of the page.
17
18
  - The pill turns green and writes one `:KnowledgeDocument` plus the entities the body assertively states.
18
19
 
19
- If the pill turns amber and shows **Sign in to SiteDesk**, your session key has expired — open the options page and paste a fresh one.
20
+ The pill tells you which thing is wrong:
21
+
22
+ | Pill | Meaning | Fix |
23
+ |---|---|---|
24
+ | **Sign in to SiteDesk** (amber) | the session key expired | open options, paste a fresh key |
25
+ | **Grant access** (amber) | the admin host was never allowed, so the post never left the browser | open options, Save, allow the prompt |
26
+ | **SiteDesk unreachable** (red) | the host was allowed but did not answer | check the tunnel and the admin server |
27
+ | **Capture failed** (red) | the admin rejected the payload, or the extension's own service worker did not answer | read the service-worker console: `reason=extract` is a rejected payload, `reason=worker-unreachable` is the worker |
20
28
 
21
29
  ## File layout
22
30
 
@@ -26,7 +34,7 @@ If the pill turns amber and shows **Sign in to SiteDesk**, your session key has
26
34
  | `content/extractors.js` | Pure DOM extractor functions. Loaded first into each content-script context. |
27
35
  | `content/profile.js` | Profile pill + click handler. |
28
36
  | `content/thread.js` | Thread pill + click handler. |
29
- | `background/sw.js` | Service worker. POSTs envelopes to the admin route. |
37
+ | `background/sw.js` | Service worker. Checks the admin-host grant, then POSTs envelopes to the admin route. |
30
38
  | `options/options.html` + `options/options.js` | Admin host + session-key entry. |
31
39
  | `assets/pill.css` | Pill styling. |
32
40
  | `__tests__/` | jsdom unit tests for the extractors, run via vitest from `platform/ui`. |
@@ -1,5 +1,16 @@
1
1
  // Service worker. Receives envelopes from content scripts, POSTs them
2
- // to the admin host with the bearer token stored in extension options.
2
+ // to the admin host with the session key stored in extension options.
3
+ //
4
+ // Task 2185 — the manifest's static host_permissions cover linkedin.com only,
5
+ // so the admin host has to be granted at runtime from the options page before
6
+ // this fetch is permission-exempt. Without the grant Chrome treats the POST as
7
+ // an ordinary cross-origin request and preflights it (the content type is
8
+ // application/json, which is not CORS-safelisted); the admin route answers no
9
+ // preflight, so the request dies in the browser and the server never sees it.
10
+ //
11
+ // The grant is checked BEFORE the fetch on purpose: a missing grant and an
12
+ // unreachable host both surface as the same `TypeError: Failed to fetch`, so
13
+ // asking first is the only way to tell an operator which one they have.
3
14
 
4
15
  const TAG = '[linkedin-ext-post]'
5
16
 
@@ -11,11 +22,30 @@ async function getConfig() {
11
22
  }
12
23
  }
13
24
 
25
+ // Chrome match pattern covering every path on the admin host's origin.
26
+ function originPattern(adminHost) {
27
+ return new URL(adminHost).origin + '/*'
28
+ }
29
+
14
30
  async function postEnvelope(envelope) {
15
31
  const { adminHost, sessionKey } = await getConfig()
16
32
  if (!adminHost) return { ok: false, reason: 'auth', detail: 'admin-host-missing' }
17
33
  if (!sessionKey) return { ok: false, reason: 'auth', detail: 'session-key-missing' }
18
34
 
35
+ let origin
36
+ try {
37
+ origin = originPattern(adminHost)
38
+ } catch {
39
+ return { ok: false, reason: 'permission', detail: 'admin-host-not-a-url' }
40
+ }
41
+
42
+ const granted = await chrome.permissions.contains({ origins: [origin] })
43
+ console.log(
44
+ TAG + ' op=permission dispatchId=' + envelope.dispatchId +
45
+ ' origin=' + origin + ' granted=' + granted,
46
+ )
47
+ if (!granted) return { ok: false, reason: 'permission', detail: 'host-not-granted origin=' + origin }
48
+
19
49
  const url = adminHost + '/api/admin/linkedin-ingest'
20
50
  const started = Date.now()
21
51
  let res
@@ -26,12 +56,15 @@ async function postEnvelope(envelope) {
26
56
  body: JSON.stringify({ ...envelope, session_key: sessionKey }),
27
57
  })
28
58
  } catch (err) {
29
- console.error(TAG + ' dispatchId=' + envelope.dispatchId + ' status=network ms=' + (Date.now() - started))
30
- return { ok: false, reason: 'network', detail: err && err.message ? err.message : String(err) }
59
+ console.error(
60
+ TAG + ' op=fetch dispatchId=' + envelope.dispatchId +
61
+ ' status=unreachable ms=' + (Date.now() - started),
62
+ )
63
+ return { ok: false, reason: 'unreachable', detail: err && err.message ? err.message : String(err) }
31
64
  }
32
65
 
33
66
  const ms = Date.now() - started
34
- console.log(TAG + ' dispatchId=' + envelope.dispatchId + ' status=' + res.status + ' ms=' + ms)
67
+ console.log(TAG + ' op=fetch dispatchId=' + envelope.dispatchId + ' status=' + res.status + ' ms=' + ms)
35
68
 
36
69
  if (res.status === 401) return { ok: false, reason: 'auth', detail: 'session-rejected' }
37
70
  if (res.status >= 500) return { ok: false, reason: 'server', detail: 'http-' + res.status }
@@ -38,13 +38,17 @@
38
38
  try {
39
39
  reply = await chrome.runtime.sendMessage({ type: 'linkedin-ingest', envelope: envelope })
40
40
  } catch (err) {
41
- console.error(TAG_ERROR + ' reason=network detail=' + (err && err.message ? err.message : String(err)))
41
+ console.error(TAG_ERROR + ' reason=worker-unreachable detail=' + (err && err.message ? err.message : String(err)))
42
42
  setState(pill, 'error', 'Capture failed')
43
43
  return
44
44
  }
45
45
 
46
46
  if (!reply || !reply.ok) {
47
- const reason = (reply && reply.reason) || 'network'
47
+ // `worker-unreachable` is the extension's OWN service worker failing to
48
+ // answer. It is not `unreachable`, which means the admin host did not
49
+ // answer — blaming SiteDesk here sends the operator to check a tunnel that
50
+ // was never involved.
51
+ const reason = (reply && reply.reason) || 'worker-unreachable'
48
52
  const detail = (reply && reply.detail) || ''
49
53
  console.error(TAG_ERROR + ' reason=' + reason + ' detail=' + detail)
50
54
  if (reason === 'auth') {
@@ -52,6 +56,17 @@
52
56
  pill.onclick = function () { chrome.runtime.openOptionsPage() }
53
57
  return
54
58
  }
59
+ // Task 2185 — the admin host was never granted, so the POST never left
60
+ // the browser. Sending the operator to the options page is the fix.
61
+ if (reason === 'permission') {
62
+ setState(pill, 'auth', 'Grant access')
63
+ pill.onclick = function () { chrome.runtime.openOptionsPage() }
64
+ return
65
+ }
66
+ if (reason === 'unreachable') {
67
+ setState(pill, 'error', 'SiteDesk unreachable')
68
+ return
69
+ }
55
70
  setState(pill, 'error', 'Capture failed')
56
71
  return
57
72
  }
@@ -40,13 +40,17 @@
40
40
  try {
41
41
  reply = await chrome.runtime.sendMessage({ type: 'linkedin-ingest', envelope: envelope })
42
42
  } catch (err) {
43
- console.error(TAG_ERROR + ' reason=network detail=' + (err && err.message ? err.message : String(err)))
43
+ console.error(TAG_ERROR + ' reason=worker-unreachable detail=' + (err && err.message ? err.message : String(err)))
44
44
  setState(pill, 'error', 'Capture failed')
45
45
  return
46
46
  }
47
47
 
48
48
  if (!reply || !reply.ok) {
49
- const reason = (reply && reply.reason) || 'network'
49
+ // `worker-unreachable` is the extension's OWN service worker failing to
50
+ // answer. It is not `unreachable`, which means the admin host did not
51
+ // answer — blaming SiteDesk here sends the operator to check a tunnel that
52
+ // was never involved.
53
+ const reason = (reply && reply.reason) || 'worker-unreachable'
50
54
  const detail = (reply && reply.detail) || ''
51
55
  console.error(TAG_ERROR + ' reason=' + reason + ' detail=' + detail)
52
56
  if (reason === 'auth') {
@@ -54,6 +58,17 @@
54
58
  pill.onclick = function () { chrome.runtime.openOptionsPage() }
55
59
  return
56
60
  }
61
+ // Task 2185 — the admin host was never granted, so the POST never left
62
+ // the browser. Sending the operator to the options page is the fix.
63
+ if (reason === 'permission') {
64
+ setState(pill, 'auth', 'Grant access')
65
+ pill.onclick = function () { chrome.runtime.openOptionsPage() }
66
+ return
67
+ }
68
+ if (reason === 'unreachable') {
69
+ setState(pill, 'error', 'SiteDesk unreachable')
70
+ return
71
+ }
57
72
  setState(pill, 'error', 'Capture failed')
58
73
  return
59
74
  }
@@ -5,6 +5,7 @@
5
5
  "description": "Add an Add-to-SiteDesk pill to LinkedIn profiles and DM threads. One click captures the page and posts to your SiteDesk admin.",
6
6
  "permissions": ["storage", "activeTab"],
7
7
  "host_permissions": ["https://www.linkedin.com/*"],
8
+ "optional_host_permissions": ["http://*/*", "https://*/*"],
8
9
  "background": {
9
10
  "service_worker": "background/sw.js"
10
11
  },
@@ -18,6 +18,7 @@
18
18
  <body>
19
19
  <h1>SiteDesk LinkedIn Ingest</h1>
20
20
  <p>Point the extension at your SiteDesk admin host and paste your admin session key. The pill on LinkedIn will then post one-click captures to your admin.</p>
21
+ <p>On Save, Chrome asks you to allow this extension to talk to that host. The capture pill cannot post without it, so declining leaves the extension unconfigured.</p>
21
22
 
22
23
  <label for="admin-host">Admin host</label>
23
24
  <input id="admin-host" type="url" placeholder="https://your-tunnel.example.com" />