@rho-workspace/rho 0.5.5 → 0.5.7

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 (250) hide show
  1. package/dist/agent-connect.d.ts +42 -0
  2. package/dist/agent-connect.js +68 -0
  3. package/dist/answer-board.js +3 -3
  4. package/dist/bridge.js +6 -1
  5. package/dist/browser-access.d.ts +1 -0
  6. package/dist/browser-access.js +5 -3
  7. package/dist/capabilities.d.ts +21 -5
  8. package/dist/capabilities.js +6 -4
  9. package/dist/cli.js +113 -18
  10. package/dist/composition.d.ts +1 -1
  11. package/dist/composition.js +13 -3
  12. package/dist/connect-cli.d.ts +50 -0
  13. package/dist/connect-cli.js +128 -0
  14. package/dist/contract.generated.d.ts +1437 -381
  15. package/dist/contract.generated.js +1 -1
  16. package/dist/contracts/owners.generated.js +1160 -267
  17. package/dist/conversation-presentation.d.ts +9 -2
  18. package/dist/conversation-presentation.js +3 -1
  19. package/dist/conversations.d.ts +306 -7
  20. package/dist/conversations.js +208 -22
  21. package/dist/discovery.js +3 -3
  22. package/dist/extension-contract.d.ts +19 -0
  23. package/dist/extension-contract.js +15 -6
  24. package/dist/extension-markets.d.ts +74 -0
  25. package/dist/extension-markets.js +166 -0
  26. package/dist/extensions.d.ts +29 -1
  27. package/dist/extensions.js +13 -4
  28. package/dist/external-work.d.ts +22 -0
  29. package/dist/external-work.js +91 -0
  30. package/dist/plugin-cli.js +75 -3
  31. package/dist/plugin-inventory.d.ts +56 -0
  32. package/dist/plugin-inventory.js +41 -0
  33. package/dist/plugin-inventory.json +1581 -102
  34. package/dist/plugin-presentation.d.ts +105 -2
  35. package/dist/plugin-presentation.js +59 -5
  36. package/dist/plugins.d.ts +14 -3
  37. package/dist/plugins.js +12 -6
  38. package/dist/project-notes.js +1 -1
  39. package/dist/protocol.d.ts +7 -0
  40. package/dist/protocol.js +40 -15
  41. package/dist/replay.js +4 -0
  42. package/dist/response-contracts.js +26 -11
  43. package/dist/running.d.ts +10 -0
  44. package/dist/running.js +45 -0
  45. package/dist/server.d.ts +4 -10
  46. package/dist/server.js +140 -14
  47. package/dist/state.d.ts +8 -0
  48. package/dist/state.js +8 -0
  49. package/dist/studio-layout.js +3 -3
  50. package/dist/task-frame.d.ts +1 -1
  51. package/dist/timeline-events.d.ts +1 -0
  52. package/dist/timeline-events.js +8 -1
  53. package/dist/ui-context.d.ts +73 -0
  54. package/dist/ui-context.js +98 -0
  55. package/dist/ui-contract.d.ts +23 -0
  56. package/dist/web/.vite/manifest.json +135 -76
  57. package/dist/web/assets/answer-board-CeYS8k8y.js +1 -0
  58. package/dist/web/assets/choices-D8vGwsl2.js +1 -0
  59. package/dist/web/assets/{console-BSjLGe0T.js → console-Dyuo1q4N.js} +4 -4
  60. package/dist/web/assets/{dist-Tzoe_zm-.js → dist-CSiCyBml.js} +1 -1
  61. package/dist/web/assets/{dist-Cw8hRIlT.js → dist-D9FLb9Hm.js} +1 -1
  62. package/dist/web/assets/{dist-foH9IKc6.js → dist-FXbTBwML.js} +1 -1
  63. package/dist/web/assets/{dist-Buk0lwzI.js → dist-l3DUhbw5.js} +1 -1
  64. package/dist/web/assets/{explore-in-BVUy3ZW_.js → explore-in-BfSFGRh2.js} +1 -1
  65. package/dist/web/assets/graph-BU2_o5uA.js +2 -0
  66. package/dist/web/assets/index-C16mXVvk.css +1 -0
  67. package/dist/web/assets/index-CWg9XYXx.js +952 -0
  68. package/dist/web/assets/markdown.reader-Ct2-e2Zt.png +0 -0
  69. package/dist/web/assets/{notes-meaning-pVbbQUGP.js → notes-meaning-T2m5zQfv.js} +1 -1
  70. package/dist/web/assets/overview-PzfLJCo-.js +6 -0
  71. package/dist/web/assets/preview-BguEdsza.js +1 -0
  72. package/dist/web/assets/previews-pWNik-2g.js +3 -0
  73. package/dist/web/assets/{review-CMxQlQ-C.js → review-Dy_85lcH.js} +1 -1
  74. package/dist/web/assets/rho.editor-CCJeyHma.png +0 -0
  75. package/dist/web/assets/rho.flow-WkMj6fRS.png +0 -0
  76. package/dist/web/assets/rho.media-Cc6amodO.png +0 -0
  77. package/dist/web/assets/rho.notebook-6vsZKGDq.png +0 -0
  78. package/dist/web/assets/rho.object-BGoeXjte.png +0 -0
  79. package/dist/web/assets/rho.plots-yIiISCkb.png +0 -0
  80. package/dist/web/assets/rho.scatter-DwHxesCs.png +0 -0
  81. package/dist/web/assets/rho.servers-DKf_mnNq.png +0 -0
  82. package/dist/web/assets/rho.table-DWOGaWDa.png +0 -0
  83. package/dist/web/assets/rho.variables-CKcWczhQ.png +0 -0
  84. package/dist/web/assets/{skills-BFNSyrnD.js → skills-Dyz7mBlc.js} +1 -1
  85. package/dist/web/assets/{skills-work-BGPSnF_I.js → skills-work-BEGqlCkx.js} +1 -1
  86. package/dist/web/assets/studio-process-D-ykbC4D.js +2 -0
  87. package/dist/web/assets/timeline-events-DKTho9fU.js +1 -0
  88. package/dist/web/assets/work-turns-DSabCogR.js +1 -0
  89. package/dist/web/assets/{workspace-CWlkjLNx.js → workspace-DfjBL--r.js} +2 -2
  90. package/dist/web/composition.json +135 -80
  91. package/dist/web/index.html +3 -3
  92. package/dist/web/renderer.json +1 -1
  93. package/dist/web-assets.js +2 -2
  94. package/dist/work-graph.d.ts +11 -1
  95. package/dist/work-graph.js +16 -5
  96. package/dist/workspace.d.ts +2 -2
  97. package/dist/workspace.js +8 -7
  98. package/docs/FRONTEND.md +71 -21
  99. package/docs/HEADLESS.md +127 -24
  100. package/docs/THIRD-PARTY-PLUGINS.md +75 -3
  101. package/node_modules/@rho-workspace/plugin-agent/README.md +34 -0
  102. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +32 -0
  103. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +54 -11
  104. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.d.ts +1 -0
  105. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +29 -3
  106. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +11 -5
  107. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +12 -1
  108. package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +8 -0
  109. package/node_modules/@rho-workspace/plugin-agent/dist/index.js +3 -2
  110. package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.d.ts +13 -0
  111. package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.js +13 -0
  112. package/node_modules/@rho-workspace/plugin-agent/dist/mcp.js +16 -8
  113. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +70 -0
  114. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.js +538 -0
  115. package/node_modules/@rho-workspace/plugin-agent/package.json +2 -1
  116. package/node_modules/@rho-workspace/plugin-figures/LICENSE +661 -0
  117. package/node_modules/@rho-workspace/plugin-figures/README.md +191 -0
  118. package/node_modules/@rho-workspace/plugin-figures/dist/index.d.ts +236 -0
  119. package/node_modules/@rho-workspace/plugin-figures/dist/index.js +165 -0
  120. package/node_modules/@rho-workspace/plugin-figures/dist/operations.d.ts +44 -0
  121. package/node_modules/@rho-workspace/plugin-figures/dist/operations.js +39 -0
  122. package/node_modules/@rho-workspace/plugin-figures/dist/presentation.d.ts +19 -0
  123. package/node_modules/@rho-workspace/plugin-figures/dist/presentation.js +26 -0
  124. package/node_modules/@rho-workspace/plugin-figures/dist/record.d.ts +188 -0
  125. package/node_modules/@rho-workspace/plugin-figures/dist/record.js +143 -0
  126. package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.d.ts +1 -0
  127. package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.js +68 -0
  128. package/node_modules/@rho-workspace/plugin-figures/dist/ui.d.ts +56 -0
  129. package/node_modules/@rho-workspace/plugin-figures/dist/ui.js +363 -0
  130. package/node_modules/@rho-workspace/plugin-figures/package.json +41 -0
  131. package/node_modules/@rho-workspace/plugin-figures/py/figures.py +856 -0
  132. package/node_modules/@rho-workspace/plugin-figures/r/figures.R +775 -0
  133. package/node_modules/@rho-workspace/plugin-kernel/README.md +27 -3
  134. package/node_modules/@rho-workspace/plugin-kernel/dist/contract.d.ts +12 -0
  135. package/node_modules/@rho-workspace/plugin-kernel/dist/contract.js +8 -1
  136. package/node_modules/@rho-workspace/plugin-kernel/dist/index.d.ts +61 -0
  137. package/node_modules/@rho-workspace/plugin-kernel/dist/index.js +125 -9
  138. package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.d.ts +14 -0
  139. package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.js +9 -0
  140. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-base.d.ts +23 -5
  141. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook-work.js +1 -1
  142. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.d.ts +6 -4
  143. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.js +44 -17
  144. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-styles.js +6 -0
  145. package/node_modules/@rho-workspace/plugin-kernel/package.json +1 -1
  146. package/node_modules/@rho-workspace/plugin-kernel/runners/runner.R +144 -4
  147. package/node_modules/@rho-workspace/plugin-kernel/runners/runner.py +106 -1
  148. package/node_modules/@rho-workspace/plugin-tables/README.md +5 -0
  149. package/node_modules/@rho-workspace/plugin-tables/dist/index.d.ts +1 -0
  150. package/node_modules/@rho-workspace/plugin-tables/dist/index.js +19 -3
  151. package/node_modules/@rho-workspace/plugin-tables/dist/ui.js +1 -1
  152. package/node_modules/@rho-workspace/plugin-tables/package.json +1 -1
  153. package/node_modules/hono/dist/adapter/aws-lambda/handler.js +1 -1
  154. package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +1 -1
  155. package/node_modules/hono/dist/cjs/adapter/bun/serve-static.js +1 -1
  156. package/node_modules/hono/dist/cjs/adapter/bun/websocket.js +1 -1
  157. package/node_modules/hono/dist/cjs/client/fetch-result-please.js +1 -1
  158. package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +2 -1
  159. package/node_modules/hono/dist/cjs/helper/ssg/ssg.js +5 -3
  160. package/node_modules/hono/dist/cjs/jsx/base.js +49 -30
  161. package/node_modules/hono/dist/cjs/jsx/components.js +2 -2
  162. package/node_modules/hono/dist/cjs/jsx/constants.js +2 -0
  163. package/node_modules/hono/dist/cjs/jsx/jsx-runtime.js +13 -17
  164. package/node_modules/hono/dist/cjs/middleware/etag/index.js +3 -1
  165. package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +1 -0
  166. package/node_modules/hono/dist/cjs/middleware/timing/timing.js +4 -3
  167. package/node_modules/hono/dist/cjs/utils/cookie.js +2 -2
  168. package/node_modules/hono/dist/cjs/utils/mime.js +1 -1
  169. package/node_modules/hono/dist/cjs/utils/url.js +1 -1
  170. package/node_modules/hono/dist/client/fetch-result-please.js +1 -1
  171. package/node_modules/hono/dist/helper/accepts/accepts.js +2 -1
  172. package/node_modules/hono/dist/helper/ssg/ssg.js +5 -3
  173. package/node_modules/hono/dist/jsx/base.js +49 -32
  174. package/node_modules/hono/dist/jsx/components.js +2 -2
  175. package/node_modules/hono/dist/jsx/constants.js +2 -1
  176. package/node_modules/hono/dist/jsx/jsx-runtime.js +15 -19
  177. package/node_modules/hono/dist/middleware/etag/index.js +3 -1
  178. package/node_modules/hono/dist/middleware/language/language.js +1 -1
  179. package/node_modules/hono/dist/middleware/pretty-json/index.js +1 -0
  180. package/node_modules/hono/dist/middleware/timing/timing.js +4 -3
  181. package/node_modules/hono/dist/utils/cookie.js +2 -2
  182. package/node_modules/hono/dist/utils/mime.js +1 -1
  183. package/node_modules/hono/dist/utils/url.js +1 -1
  184. package/node_modules/hono/package.json +1 -1
  185. package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +3 -2
  186. package/node_modules/jose/dist/webapi/jwks/remote.js +1 -1
  187. package/node_modules/jose/dist/webapi/jws/general/verify.js +2 -8
  188. package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +11 -8
  189. package/node_modules/jose/dist/webapi/lib/key_management.js +0 -1
  190. package/node_modules/jose/package.json +1 -1
  191. package/package.json +6 -4
  192. package/protocol/index.json +106 -64
  193. package/protocol/operations/agent.sessions.list.json +57 -3
  194. package/protocol/operations/agent.sessions.read.json +61 -4
  195. package/protocol/operations/capability.list.json +382 -6
  196. package/protocol/operations/composition.apply.json +10 -2
  197. package/protocol/operations/composition.delete.json +845 -0
  198. package/protocol/operations/composition.read.json +10 -2
  199. package/protocol/operations/composition.undo.json +10 -2
  200. package/protocol/operations/conversation.agents.add.json +346 -0
  201. package/protocol/operations/conversation.agents.remove.json +340 -0
  202. package/protocol/operations/conversation.configure.json +29 -0
  203. package/protocol/operations/conversation.decide.json +225 -0
  204. package/protocol/operations/conversation.list.json +29 -0
  205. package/protocol/operations/conversation.permission.json +225 -0
  206. package/protocol/operations/conversation.read.json +225 -0
  207. package/protocol/operations/conversation.restore.json +29 -0
  208. package/protocol/operations/conversation.send.json +294 -0
  209. package/protocol/operations/conversation.settings.check.json +226 -416
  210. package/protocol/operations/conversation.settings.read.json +224 -411
  211. package/protocol/operations/conversation.settings.update.json +248 -475
  212. package/protocol/operations/conversation.start.json +313 -6
  213. package/protocol/operations/conversation.stop.json +225 -0
  214. package/protocol/operations/conversation.trash.json +29 -0
  215. package/protocol/operations/extension.describe.json +39 -0
  216. package/protocol/operations/extension.install.json +62 -0
  217. package/protocol/operations/extension.list.json +164 -1
  218. package/protocol/operations/extension.market.change.json +210 -0
  219. package/protocol/operations/figure.read.json +265 -0
  220. package/protocol/operations/figure.select.json +157 -0
  221. package/protocol/operations/kernel.interrupt.json +119 -0
  222. package/protocol/operations/kernel.read.json +119 -0
  223. package/protocol/operations/kernel.run.json +119 -0
  224. package/protocol/operations/observation.snapshot.json +445 -8
  225. package/protocol/operations/project.openFile.json +10 -2
  226. package/protocol/operations/session.grant.json +4 -0
  227. package/protocol/operations/session.instructions.json +43 -0
  228. package/protocol/operations/shared.json +1 -0
  229. package/protocol/operations/view.close.json +10 -2
  230. package/protocol/operations/view.protection.set.json +10 -2
  231. package/vendor/composition.json +174 -109
  232. package/vendor/rho-workspace-plugin-agent-0.4.33.tgz +0 -0
  233. package/vendor/rho-workspace-plugin-figures-0.5.1.tgz +0 -0
  234. package/vendor/rho-workspace-plugin-kernel-0.5.13.tgz +0 -0
  235. package/vendor/rho-workspace-plugin-tables-0.5.2.tgz +0 -0
  236. package/web/theme.css +17 -0
  237. package/dist/web/assets/answer-board-CBkGbSLD.js +0 -1
  238. package/dist/web/assets/canvas-D8rDdfDz.js +0 -1
  239. package/dist/web/assets/choices-b0XhrGQV.js +0 -1
  240. package/dist/web/assets/graph-1OdIJ9l7.js +0 -2
  241. package/dist/web/assets/index-CsC3sTRq.css +0 -1
  242. package/dist/web/assets/index-CxmItXqa.js +0 -891
  243. package/dist/web/assets/overview-B9UtVJ32.js +0 -6
  244. package/dist/web/assets/preview-CksD_5KS.js +0 -1
  245. package/dist/web/assets/studio-process-CsRoTHOh.js +0 -2
  246. package/dist/web/assets/timeline-events-BbuuFWHV.js +0 -1
  247. package/dist/web/assets/work-turns-BvJlIMmA.js +0 -1
  248. package/vendor/rho-workspace-plugin-agent-0.4.29.tgz +0 -0
  249. package/vendor/rho-workspace-plugin-kernel-0.5.5.tgz +0 -0
  250. package/vendor/rho-workspace-plugin-tables-0.5.0.tgz +0 -0
@@ -164,29 +164,44 @@ export const pluginPresentationSchema = { ...entryPresentationSchema, properties
164
164
  examplePrompts: { type: 'array', items: text, maxItems: 16 }, viewNote: text, developer: text, license: text,
165
165
  links: { type: 'array', maxItems: 16, items: obj({ label: text, url: text }, ['label', 'url']) },
166
166
  dependencies: { type: 'array', maxItems: 16, items: obj({ pluginId: text, purpose: text }, ['pluginId', 'purpose']) }, uses: text } };
167
+ const skillListingSchema = { ...obj({
168
+ id: text, version: text, digest, presentation: entryPresentationSchema, intent: text,
169
+ requires: { type: 'array', items: text, maxItems: 64 },
170
+ example: obj({ method: text, input: { type: 'object' } }),
171
+ failureRecipes: { type: 'array', maxItems: 32, items: obj({ kind: text, message: text, next: text }) },
172
+ verify: obj({ command: text, description: text })
173
+ }, ['id', 'version', 'digest']), additionalProperties: true };
167
174
  export const capabilityCatalogSchema = { ...obj({
168
175
  version: text,
169
176
  configuration: obj({ version: integer, enabled: { type: 'array', items: text, maxItems: 32 }, retainedReceipts: integer, retainedFromVersion: positive, maxReceipts: integer }, ['version', 'enabled']),
170
- plugins: { type: 'array', maxItems: 16, items: { ...obj({ id: text, package: text, version: text, requires: { type: 'array', items: text }, selected: { type: 'boolean' }, packagePresent: { type: 'boolean' }, projectEnabled: { type: 'boolean' }, dependenciesSatisfied: { type: 'boolean' }, uiRenderable: { type: 'boolean' }, state: { enum: ['available', 'unavailable'] }, reason: text, missing: { type: 'array', items: text },
177
+ plugins: { type: 'array', maxItems: 16, items: { ...obj({ id: text, package: text, version: text, packageVersion: text, requires: { type: 'array', items: text }, selected: { type: 'boolean' }, packagePresent: { type: 'boolean' }, projectEnabled: { type: 'boolean' }, dependenciesSatisfied: { type: 'boolean' }, uiRenderable: { type: 'boolean' }, state: { enum: ['available', 'unavailable'] }, reason: text, missing: { type: 'array', items: text },
171
178
  // The plugin's read whose events (who did what, when) the timeline draws.
172
179
  timeline: text,
173
180
  // The catalog also carries the owner's own presentation/listing hints and its
174
181
  // bounded native availability; those are owner vocabulary, so they stay open.
175
182
  // A reduced installation lists fewer entries, so each listing section is optional.
176
183
  listing: obj({
177
- skills: { type: 'array', maxItems: 64, items: { ...obj({ id: text, version: text, digest: digest, presentation: entryPresentationSchema }, ['id', 'version', 'digest']), additionalProperties: true } },
178
- actions: { type: 'array', maxItems: 64, items: { ...obj({ presentation: entryPresentationSchema }), additionalProperties: true } },
179
- views: { type: 'array', maxItems: 64, items: { ...obj({ id: text, version: text, ui: text, presentation: entryPresentationSchema }, ['id', 'version', 'ui']), additionalProperties: true } }
184
+ skills: { type: 'array', maxItems: 64, items: skillListingSchema },
185
+ operations: { type: 'array', maxItems: 64, items: obj({ method: text, version: text, effect: { enum: ['read', 'metadata', 'document', 'file', 'native'] }, requires: { type: 'array', items: text }, presentation: entryPresentationSchema }, ['method', 'version', 'effect', 'requires']) },
186
+ actions: { type: 'array', maxItems: 64, items: { ...obj({ id: text, version: text, requires: { type: 'array', items: text }, presentation: entryPresentationSchema }, []), additionalProperties: true } },
187
+ views: { type: 'array', maxItems: 64, items: { ...obj({ id: text, version: text, ui: text, bindings: { type: 'object', additionalProperties: text }, presentation: entryPresentationSchema }, ['id', 'version', 'ui']), additionalProperties: true } },
188
+ // Command-line entries the installed package links, read from its own manifest.
189
+ cli: { type: 'array', maxItems: 16, items: text },
190
+ mcp: obj({ entry: text, transport: text, tools: { type: 'array', maxItems: 32, items: obj({ name: text, description: text }) } }),
191
+ claudeCode: obj({
192
+ skills: { type: 'array', maxItems: 64, items: obj({ skill: text, name: text, description: text }) },
193
+ hooks: { type: 'array', maxItems: 64, items: obj({ id: text, event: text, matcher: text, entry: text, timeoutSeconds: positive }, ['event', 'matcher', 'entry']) }
194
+ }, [])
180
195
  }, []),
181
196
  presentation: pluginPresentationSchema,
182
197
  nativeAvailability: nativeAvailabilitySchema,
183
198
  }, ['id', 'package', 'selected', 'packagePresent', 'state']), additionalProperties: true } },
184
- operations: { type: 'array', maxItems: 256, items: { ...obj({ method: text, owner: text, packagePresent: { type: 'boolean' }, projectEnabled: { type: 'boolean' }, dependenciesSatisfied: { type: 'boolean' }, authorized: { type: 'boolean' }, state: { enum: ['available', 'unavailable'] }, presentation: entryPresentationSchema }, ['method', 'authorized', 'state']), additionalProperties: true } },
199
+ operations: { type: 'array', maxItems: 256, items: { ...obj({ method: text, owner: text, effect: text, inputSchema: { type: 'object' }, outputSchema: { type: 'object' }, packagePresent: { type: 'boolean' }, projectEnabled: { type: 'boolean' }, dependenciesSatisfied: { type: 'boolean' }, authorized: { type: 'boolean' }, state: { enum: ['available', 'unavailable'] }, presentation: entryPresentationSchema }, ['method', 'authorized', 'state']), additionalProperties: true } },
185
200
  actions: { type: 'array', maxItems: 64, items: { ...obj({ id: text, version: text, owner: text, pluginId: text, requires: { type: 'array', items: text }, inputSchema: { type: 'object' }, presentation: entryPresentationSchema }, ['id', 'version']), additionalProperties: true } },
186
201
  unavailableActions: { type: 'array', maxItems: 64, items: { ...obj({ id: text, version: text, owner: text, pluginId: text, requires: { type: 'array', items: text }, inputSchema: { type: 'object' }, presentation: entryPresentationSchema, state: { const: 'unavailable' }, missing: { type: 'array', items: text } }, ['id', 'version', 'missing']), additionalProperties: true } },
187
- components: { type: 'array', maxItems: 64, items: { ...obj({ id: text, version: text, owner: text, pluginId: text, ui: text, bindings: { type: 'object', additionalProperties: text }, propsSchema: { type: 'object' }, presentation: entryPresentationSchema }, ['id', 'version', 'ui', 'bindings']), additionalProperties: true } },
202
+ components: { type: 'array', maxItems: 64, items: { ...obj({ id: text, version: text, owner: text, pluginId: text, ui: text, uiPackage: text, uiPackageVersion: text, bindings: { type: 'object', additionalProperties: text }, propsSchema: { type: 'object' }, presentation: entryPresentationSchema }, ['id', 'version', 'ui', 'bindings']), additionalProperties: true } },
188
203
  capabilities: { type: 'array', items: text },
189
- skills: { type: 'array', maxItems: 64, items: { ...obj({ id: text, version: text, digest: digest, content: { type: 'string' }, presentation: entryPresentationSchema }, ['id', 'version', 'digest']), additionalProperties: true } },
204
+ skills: { type: 'array', maxItems: 64, items: { ...skillListingSchema, properties: { ...skillListingSchema.properties, owner: text, content: { type: 'string' } } } },
190
205
  // The Agent owner contributes its own context description only when it is loaded.
191
206
  agent: { type: 'object' }
192
207
  }, ['version', 'configuration', 'plugins', 'operations', 'actions', 'components']), additionalProperties: true };
@@ -198,8 +213,8 @@ export const nodeSchema = { oneOf: [
198
213
  obj({ id: identity, kind: { const: 'group' }, children: { type: 'array', items: identity, maxItems: 256 }, presentation: groupPresentation }, ['id', 'kind', 'children']),
199
214
  { ...viewSchema, properties: { ...viewSchema.properties, kind: { const: 'view' } }, required: [...viewSchema.required, 'kind'] }
200
215
  ] };
201
- export const compositionReceiptSchema = obj({ changeId: identity, author: text, version: positive, requestDigest: digest, at: timestamp, kind: { enum: ['apply', 'undo', 'protect'] } });
202
- const historyEntry = obj({ version: positive, snapshot: obj({ rootId: identity, nodes: { type: 'array', items: nodeSchema, maxItems: 256 } }) }, ['version', 'snapshot']);
216
+ export const compositionReceiptSchema = obj({ changeId: identity, author: text, version: positive, requestDigest: digest, at: timestamp, kind: { enum: ['apply', 'undo', 'protect', 'delete'] } });
217
+ const historyEntry = obj({ version: positive, snapshot: obj({ rootId: identity, nodes: { type: 'array', items: nodeSchema, maxItems: 256 }, deleted: { type: 'boolean' } }, ['rootId', 'nodes']) }, ['version', 'snapshot']);
203
218
  export const witnessSchema = { ...obj({
204
219
  witnessId: digest, projectId: identity, taskId: identity, compositionId: identity, compositionVersion: positive,
205
220
  sourceEvent: { type: 'object', additionalProperties: true }, policyRef: digest,
@@ -208,13 +223,13 @@ export const witnessSchema = { ...obj({
208
223
  at: timestamp
209
224
  }, ['witnessId', 'projectId', 'taskId', 'compositionId', 'compositionVersion', 'views']) };
210
225
  export const structuredCompositionSchema = { ...obj({
211
- format: { const: 'rho.composition/1' }, compositionId: identity, rootId: identity,
226
+ format: { const: 'rho.composition/1' }, compositionId: identity, rootId: identity, deleted: { type: 'boolean' },
212
227
  nodes: { type: 'array', items: nodeSchema, maxItems: 256 },
213
228
  views: { type: 'array', items: viewSchema, maxItems: 32 },
214
229
  availability: { type: 'object', additionalProperties: availabilitySchema },
215
230
  version: positive, author: text, acknowledgedAt: timestamp,
216
231
  history: { type: 'array', items: historyEntry, maxItems: 256 },
217
- receipts: { type: 'array', items: { ...obj({ changeId: identity, requestDigest: digest, version: positive, author: text, at: timestamp, kind: { enum: ['apply', 'undo', 'protect', 'close'] } }, ['changeId', 'author', 'version', 'at']) }, maxItems: 256 },
232
+ receipts: { type: 'array', items: { ...obj({ changeId: identity, requestDigest: digest, version: positive, author: text, at: timestamp, kind: { enum: ['apply', 'undo', 'protect', 'close', 'delete'] } }, ['changeId', 'author', 'version', 'at']) }, maxItems: 256 },
218
233
  retainedFromVersion: positive, undoRetainedFromVersion: positive,
219
234
  receipt: compositionReceiptSchema,
220
235
  witness: { anyOf: [witnessSchema, { type: 'null' }] }, witnessState: { enum: ['retained', 'expired', 'pending'] },
@@ -0,0 +1,10 @@
1
+ export type Running = {
2
+ stateDirectory: string;
3
+ url: string;
4
+ pid: number;
5
+ };
6
+ export declare const runningDirectory: (env?: NodeJS.ProcessEnv) => string;
7
+ export declare function announce(note: Running, directory?: string): Promise<void>;
8
+ export declare function withdraw(stateDirectory: string, pid?: number, directory?: string): Promise<void>;
9
+ export declare function running(directory?: string): Promise<Running[]>;
10
+ export declare function announced(stateDirectory: string, directory?: string): Promise<boolean>;
@@ -0,0 +1,45 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ export const runningDirectory = (env = process.env) => env.RHO_RUNNING_DIR || path.join(os.homedir(), 'Rho', 'running');
6
+ const noteFile = (stateDirectory, directory) => path.join(directory, createHash('sha256').update(path.resolve(stateDirectory)).digest('hex').slice(0, 16) + '.json');
7
+ async function read(file) {
8
+ try {
9
+ const note = JSON.parse(await fs.readFile(file, 'utf8'));
10
+ return typeof note?.stateDirectory === 'string' && typeof note?.url === 'string' && Number.isInteger(note?.pid) ? note : null;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ const alive = (pid) => {
17
+ try {
18
+ process.kill(pid, 0);
19
+ return true;
20
+ }
21
+ catch (error) {
22
+ return error.code === 'EPERM';
23
+ }
24
+ };
25
+ export async function announce(note, directory = runningDirectory()) {
26
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
27
+ const file = noteFile(note.stateDirectory, directory), partial = `${file}.${process.pid}.tmp`;
28
+ await fs.writeFile(partial, JSON.stringify({ ...note, stateDirectory: path.resolve(note.stateDirectory) }) + '\n', { mode: 0o600 });
29
+ await fs.rename(partial, file);
30
+ }
31
+ // Only the Rho that wrote a note removes it, so a later start on the same state keeps its own.
32
+ export async function withdraw(stateDirectory, pid = process.pid, directory = runningDirectory()) {
33
+ const file = noteFile(stateDirectory, directory);
34
+ if ((await read(file))?.pid === pid)
35
+ await fs.rm(file, { force: true });
36
+ }
37
+ export async function running(directory = runningDirectory()) {
38
+ const names = (await fs.readdir(directory).catch(() => [])).filter(name => name.endsWith('.json'));
39
+ const notes = await Promise.all(names.map(name => read(path.join(directory, name))));
40
+ return notes.filter((note) => !!note && alive(note.pid));
41
+ }
42
+ export async function announced(stateDirectory, directory = runningDirectory()) {
43
+ const note = await read(noteFile(stateDirectory, directory));
44
+ return !!note && note.stateDirectory === path.resolve(stateDirectory) && alive(note.pid);
45
+ }
package/dist/server.d.ts CHANGED
@@ -116,6 +116,7 @@ export declare class RhoService {
116
116
  conversations: Conversations;
117
117
  private consoles;
118
118
  private kernels;
119
+ private figures;
119
120
  private skillHosts;
120
121
  private feedback;
121
122
  configureFeedback(projectId: string, options?: {
@@ -168,27 +169,20 @@ export declare class RhoService {
168
169
  agentConnection(projectId: string, externalSessionId: string, file: string, methods?: string[], pluginFilter?: string[], profile?: 'layered-business' | 'layered-presentation', who?: {
169
170
  label?: string;
170
171
  conversationId?: string;
171
- }): Promise<{
172
- apiVersion: "2.0";
173
- pluginFilter?: ("media" | "r" | "agent" | "files" | "skills" | "documents" | "requirements" | "tables" | "charts" | "annotations" | "console" | "kernel")[] | undefined;
174
- profile?: "layered-business" | "layered-presentation" | undefined;
175
- url: string;
176
- token: string;
177
- projectId: string;
178
- externalSessionId: string;
179
- methods: string[];
180
- }>;
172
+ }): Promise<any>;
181
173
  private authenticate;
182
174
  private handle;
183
175
  private readBody;
184
176
  private json;
185
177
  private serial;
186
178
  private recordActivity;
179
+ private agentSessions;
187
180
  private projectActivity;
188
181
  private agentSummaries;
189
182
  private dataDir;
190
183
  private consoleFor;
191
184
  private kernelFor;
185
+ private figuresFor;
192
186
  private skillsFor;
193
187
  dispatch(request: OperationRequest, principal: Principal, contractRef?: string): Promise<unknown>;
194
188
  private events;
package/dist/server.js CHANGED
@@ -19,7 +19,7 @@ import { Interactions } from './interactions.js';
19
19
  import { TaskMap } from './task-map.js';
20
20
  import { Capabilities } from './capabilities.js';
21
21
  import { BrowserAccess, localBrowserId, localBrowserPrincipal } from './browser-access.js';
22
- import { addToAgent, connectLimits, mcpCommand, mcpName, outsideLabel } from './agent-connect.js';
22
+ import { addToAgent, connectionInstructions, connectLimits, mcpCommand, mcpName, outsideLabel } from './agent-connect.js';
23
23
  import { NativeFolderPicker, nativeFolderLimits } from './native-folders.js';
24
24
  import { homedir } from 'node:os';
25
25
  import { serveWeb, servePageFrame, pageFramePath, extensionFramePath, webRoot, installedWebEntries } from './web-assets.js';
@@ -34,6 +34,7 @@ import { exitWords, stopState } from './execution-facts.js';
34
34
  import { exportRun } from './run-export.js';
35
35
  import { ownerContracts } from './contracts/owners.generated.js';
36
36
  import { Conversations } from './conversations.js';
37
+ import { captureUiContext } from './ui-context.js';
37
38
  import { describeLimits } from './file-describer.js';
38
39
  import { projectWork } from './project-work.js';
39
40
  export const serviceLimits = { maxRequestBytes: 1024 * 1024, maxEventBytes: 65536, eventRetention: 256, maxSubscriptions: 8, maxQueue: 32, maxContexts: 128, maxBindings: 64, maxActivities: 256, maxContextBytes: 1024 * 1024, maxRetainedMetadataBytes: 32 * 1024 * 1024 };
@@ -178,6 +179,20 @@ function explainFailure(e) {
178
179
  default: return ['storage', 'The operation could not be confirmed; retain the original identity and inspect its state.', { code }];
179
180
  }
180
181
  }
182
+ // `x-rho-client: name/version`, each part URI-encoded: the MCP client's own name for itself.
183
+ // Printable text only, bounded; anything else is ignored rather than shown.
184
+ function clientOf(header) {
185
+ if (typeof header !== 'string' || header.length > 300)
186
+ return null;
187
+ try {
188
+ const [name, version] = header.split('/').map(decodeURIComponent);
189
+ const ok = (value, max) => value !== undefined && value.length > 0 && value.length <= max && /^[\x20-\x7e]+$/.test(value);
190
+ return ok(name, 128) ? { name, ...(ok(version, 64) ? { version } : {}) } : null;
191
+ }
192
+ catch {
193
+ return null;
194
+ }
195
+ }
181
196
  export class RhoService {
182
197
  config;
183
198
  extensions;
@@ -203,6 +218,7 @@ export class RhoService {
203
218
  conversations;
204
219
  consoles = new Map();
205
220
  kernels = new Map();
221
+ figures = new Map();
206
222
  skillHosts = new Map();
207
223
  feedback(project, owners = this.plugins.project(project.root, project.id)) {
208
224
  const agent = owners.need('agent');
@@ -394,6 +410,9 @@ export class RhoService {
394
410
  },
395
411
  directory: project => this.dataDir(project),
396
412
  notebook: () => this.plugins.availableIds().includes('kernel'),
413
+ // start/send already holds the service's project serializer while this
414
+ // observation is frozen, before the conversation is recorded or launched.
415
+ uiContext: (project, location, limits) => captureUiContext(project, location, id => new Workspace(project, this.plugins.project(project.root, project.id), this.config.workspace).read(id), limits, this.plugins.optional('figures') ? (ref, selectionId, rows) => this.figuresFor(project).brief(ref, selectionId, rows) : undefined),
397
416
  context: async (project, id) => {
398
417
  const context = await this.plugins.project(project.root, project.id).need('agent').peekContext(id);
399
418
  requireThat(context.projectId === project.id, 'scope', 'The context belongs to another project.');
@@ -452,6 +471,7 @@ export class RhoService {
452
471
  return [await writeClaudePlugin(path.join(this.state.directory, 'claude-plugins'), { name: 'rho', version: selectedPluginVersions.agent, description: 'Skills and hooks of the plugins selected in Rho.', ...declared })];
453
472
  },
454
473
  changed: async (project, conversationId) => { void this.serial(project.id, () => this.emit(project, 'conversation.changed', conversationId, localBrowserId)).catch(() => { }); },
474
+ nativeSession: async (project, producer, sessionId) => (await import('@rho-workspace/plugin-agent/native-sessions')).nativeSession(project.root, producer, sessionId),
455
475
  // The person's own Claude Code when one is named, with no tools and in an empty folder of
456
476
  // Rho's state, so it reads only what it is given and loads no project instructions. What
457
477
  // a call cost comes from Claude Code when it knows the model's price, else from models.dev.
@@ -527,7 +547,8 @@ export class RhoService {
527
547
  await listen(0);
528
548
  }
529
549
  this.url = 'http://127.0.0.1:' + this.server.address().port;
530
- await atomicJson(connectionFile, { url: this.url, token: this.adminToken, apiVersion: '2.0' });
550
+ // The process that serves it, so rho --stop can say when it has ended and not only closed.
551
+ await atomicJson(connectionFile, { url: this.url, token: this.adminToken, apiVersion: '2.0', pid: process.pid });
531
552
  // An Agent's connection holds the address it was given. When the address changed, each one
532
553
  // that named the earlier address follows, so the Agent's next session reaches this service
533
554
  // with the grant it already has. Its token and methods are unchanged.
@@ -570,11 +591,22 @@ export class RhoService {
570
591
  requireThat(selected.every(m => !extensionManagement.includes(m) && !!operations[m] && !m.startsWith('agent.sessions.') && !m.startsWith('session.') && !m.startsWith('conversation.') && (!m.startsWith('project.') || m === 'project.entries') && !['task.routing.put', 'console.run', 'console.stop', 'kernel.stop'].includes(m)), 'invalid', 'Unknown or operator-only Agent capability.');
571
592
  requireThat(await Promise.all(selected.map(method => capabilities.isAvailable(method, state))).then(values => values.every(Boolean)), 'unavailable', 'A requested Agent operation has no selected provider.');
572
593
  requireThat(!profile || new Set(['capability.index', 'capability.resolve', ...selected]).size <= layeredGrantLimits.maxMethods, 'limit', `Layered connection methods exceed the ${layeredGrantLimits.maxMethods}-method profile; choose an explicit narrower grant.`, { limit: 'maxMethods', maximum: layeredGrantLimits.maxMethods });
573
- const token = randomBytes(32).toString('hex');
574
594
  const id = 'agent:' + digest(projectId + ':' + externalSessionId).slice(0, 32);
595
+ const grantedMethods = [...new Set([...(profile ? ['capability.index', 'capability.resolve'] : ['protocol.describe', 'capability.list']), ...selected])];
596
+ const existing = this.state.principals.find(p => p.id === id);
597
+ const kept = await fs.readFile(file, 'utf8').then(text => JSON.parse(text), () => null);
598
+ // Repeating the same setup must not disconnect a client already using its grant.
599
+ // Changed scope, expired access or a stale address still requires a fresh credential.
600
+ if (existing && !(existing.expiresAt && Date.parse(existing.expiresAt) <= Date.now())
601
+ && kept?.url === this.url && kept.projectId === projectId && kept.externalSessionId === externalSessionId
602
+ && typeof kept.token === 'string' && digest(kept.token) === existing.tokenHash
603
+ && canonical([...existing.methods].sort()) === canonical([...grantedMethods].sort())
604
+ && kept.profile === profile && canonical(existing.pluginFilter ?? []) === canonical(pluginFilter ?? []))
605
+ return kept;
606
+ const token = randomBytes(32).toString('hex');
575
607
  this.state.principals = this.state.principals.filter(p => p.id !== id);
576
608
  requireThat(!pluginFilter || pluginFilter.every(id => this.plugins.isKnown(id)), 'invalid', 'Unknown MCP plugin filter.');
577
- await this.state.addPrincipal({ id, tokenHash: digest(token), projectId, label: who.label ?? externalSessionId, ...(who.conversationId ? { conversationId: who.conversationId } : {}), methods: [...new Set([...(profile ? ['capability.index', 'capability.resolve'] : ['protocol.describe', 'capability.list']), ...selected])], ...(profile ? { profile } : {}), ...(pluginFilter ? { pluginFilter } : {}) });
609
+ await this.state.addPrincipal({ id, tokenHash: digest(token), projectId, label: who.label ?? externalSessionId, ...(who.conversationId ? { conversationId: who.conversationId } : {}), methods: grantedMethods, ...(profile ? { profile } : {}), ...(pluginFilter ? { pluginFilter } : {}) });
578
610
  const connection = { url: this.url, token, projectId, externalSessionId, methods: [...new Set([...(profile ? ['capability.index', 'capability.resolve'] : []), ...selected])], ...(profile ? { profile } : {}), ...(pluginFilter ? { pluginFilter } : {}), apiVersion: '2.0' };
579
611
  await atomicJson(file, connection);
580
612
  return connection;
@@ -671,6 +703,10 @@ export class RhoService {
671
703
  const method = url.pathname.startsWith('/api/v2/') ? url.pathname.slice(8) : '';
672
704
  requireThat(method === 'events' || !!operations[method], 'not_found', 'Unknown API operation.');
673
705
  const principal = this.authenticate(req, method);
706
+ // An Agent's MCP bridge names the client that started it; the person's screen shows it.
707
+ const named = principal.projectId && principal.kind !== 'browser' ? clientOf(req.headers['x-rho-client']) : null;
708
+ if (named)
709
+ void this.state.markClient(principal.id, named).catch(() => { });
674
710
  if (method === 'events') {
675
711
  requireThat(req.method === 'GET', 'invalid', 'Events use GET.');
676
712
  await this.subscribe(url, principal, res);
@@ -785,6 +821,33 @@ export class RhoService {
785
821
  // What the project's Agents did, for the person's screen:
786
822
  // only principals whose grant names this project, only the fields the activity record
787
823
  // keeps. A grant is the scope; an Agent need not have opened a binding.
824
+ // Every Agent session in the project folder, wherever it was started: Harness conversations Rho
825
+ // observes and the sessions Claude Code and Codex keep in their own files. Each names the Agent
826
+ // Rho continues it with and the Rho conversation that already continues it. Reads files only.
827
+ async agentSessions(p) {
828
+ const agent = this.plugins.optional('agent');
829
+ const external = agent ? agent.listExternalSessions(p.root) : { sessions: [], omitted: 0 };
830
+ const native = (await import('@rho-workspace/plugin-agent/native-sessions')).listNativeSessions(p.root);
831
+ const conversations = [...await this.conversations.list(p, {}), ...await this.conversations.list(p, { trashed: true })];
832
+ const bySession = new Map(conversations.filter(c => c.sessionId).map(c => [c.sessionId, c]));
833
+ const harness = await this.conversations.harness();
834
+ const sessions = [...external.sessions, ...native.sessions].flatMap(s => {
835
+ const conversation = bySession.get(s.sessionId);
836
+ if (conversation?.trashedAt)
837
+ return [];
838
+ const continueWith = s.producer === 'claude-code' ? 'claude-acp' : s.producer === 'codex' ? 'codex-acp' : s.producer === 'dsh' ? harness : null;
839
+ // Harness reports a turn in progress, not whether a window holds the session idle; a turn
840
+ // Rho itself is running is Rho's.
841
+ const open = s.producer === 'dsh' && s.state === 'running' && conversation?.state !== 'running' ? { where: 'DeepSeek Harness' } : s.open;
842
+ return [{ ...s, continueWith, ...(open !== undefined ? { open } : {}), ...(conversation ? { conversationId: conversation.conversationId } : {}) }];
843
+ });
844
+ // A session held only by Rho's own connection is not open elsewhere.
845
+ for (const s of sessions)
846
+ if (s.open && s.conversationId && await this.conversations.owns(p, s.conversationId, s.open.pid))
847
+ s.open = null;
848
+ sessions.sort((a, b) => b.lastAt.localeCompare(a.lastAt));
849
+ return { sessions: sessions.slice(0, 128), omitted: external.omitted + native.omitted + Math.max(0, sessions.length - 128) };
850
+ }
788
851
  async projectActivity(p) {
789
852
  const agent = this.plugins.project(p.root, p.id).agent;
790
853
  if (!agent)
@@ -799,7 +862,7 @@ export class RhoService {
799
862
  const activity = await this.projectActivity(p);
800
863
  return this.state.principals.filter(x => x.projectId === p.id && x.kind !== 'browser' && x.id !== 'operator').map(x => {
801
864
  const last = activity.entries.filter((e) => e.principal === x.id).at(-1);
802
- return { principal: x.id, label: x.label ?? null, profile: x.profile ?? null, methods: x.methods, expiresAt: x.expiresAt ?? null, ended: !!x.expiresAt && Date.parse(x.expiresAt) <= Date.now(), conversationId: x.conversationId ?? null, added: x.added ?? null,
865
+ return { principal: x.id, label: x.label ?? null, profile: x.profile ?? null, methods: x.methods, expiresAt: x.expiresAt ?? null, ended: !!x.expiresAt && Date.parse(x.expiresAt) <= Date.now(), conversationId: x.conversationId ?? null, client: x.client ?? null, added: x.added ?? null,
803
866
  last: last ? { at: String(last.at), method: String(last.method), outcome: last.outcome === 'rejected' ? 'rejected' : 'returned', ...(last.operationId ? { operationId: String(last.operationId) } : {}) } : null };
804
867
  });
805
868
  }
@@ -833,11 +896,24 @@ export class RhoService {
833
896
  return;
834
897
  pending = setTimeout(() => { pending = undefined; void this.serial(p.id, () => this.emit(p, 'kernel.changed', p.id, localBrowserId)).catch(() => { }); }, 250);
835
898
  };
836
- host = new KernelHost(p.root, { limits: this.config.kernel, rscript: this.bridge.rscript, python: this.config.python, ssh: this.config.ssh, serverctl: this.config.serverctl, onChange: changed });
899
+ // With the figures plugin, each local R and Python session describes the plots it draws.
900
+ const figures = this.plugins.optional('figures');
901
+ host = new KernelHost(p.root, { limits: this.config.kernel, rscript: this.bridge.rscript, python: this.config.python, ssh: this.config.ssh, serverctl: this.config.serverctl, onChange: changed,
902
+ ...(figures ? { extensions: { r: [figures.rExtension], python: [figures.pythonExtension] } } : {}) });
837
903
  this.kernels.set(p.id, host);
838
904
  }
839
905
  return host;
840
906
  }
907
+ // The figures the project's live R session drew, read from the records its cells kept.
908
+ figuresFor(p) {
909
+ let host = this.figures.get(p.id);
910
+ if (!host) {
911
+ const { Figures } = this.plugins.need('figures');
912
+ host = new Figures(p.root, { record: (cellId, plot) => this.kernelFor(p).figureRecord(cellId, plot), saved: sha256 => this.kernelFor(p).savedFigure(sha256) });
913
+ this.figures.set(p.id, host);
914
+ }
915
+ return host;
916
+ }
841
917
  // The project's skills, written by the person and their Agent. A change reaches open pages
842
918
  // once the request that made it has finished.
843
919
  skillsFor(p) {
@@ -869,7 +945,9 @@ export class RhoService {
869
945
  return describeProtocol(protocol, principal.methods, b);
870
946
  if (extensionManagement.includes(method)) {
871
947
  requireThat(principal.id === 'operator' || principal.id === localBrowserId, 'scope', 'Plugin installation and management belong to the person at this machine.');
872
- return method === 'extension.install' ? this.extensions.install(b.source, b.replace, b.expectedDigest) : this.extensions.manage(b.pluginId, b.expectedDigest, b.action);
948
+ if (method === 'extension.market.change')
949
+ return this.extensions.changeMarket(b);
950
+ return method === 'extension.install' ? this.extensions.install(b.source, b.replace, b.expectedDigest, b.market) : this.extensions.manage(b.pluginId, b.expectedDigest, b.action);
873
951
  }
874
952
  if (method === 'extension.list' && !b.projectId) {
875
953
  requireThat(principal.id === 'operator' || principal.id === localBrowserId, 'scope', 'A project-scoped caller must supply its project.');
@@ -1156,10 +1234,15 @@ export class RhoService {
1156
1234
  await this.emit(p, 'composition.changed', b.compositionId);
1157
1235
  return { document, viewId, composition };
1158
1236
  }
1237
+ if (method === 'session.instructions') {
1238
+ requireThat(principal.id === localBrowserId || principal.id === 'operator', 'unauthorized', 'Only the person at this machine can read Agent setup instructions.');
1239
+ return connectionInstructions(p, this.state.directory);
1240
+ }
1159
1241
  if (method === 'session.grant') {
1160
1242
  requireThat(principal.id === 'operator', 'unauthorized', 'Only the local operator may provision a project-scoped session.');
1161
1243
  const file = path.join(this.state.directory, 'agent-' + digest(p.id + ':' + b.externalSessionId) + '.json');
1162
1244
  const connection = await this.agentConnection(p.id, b.externalSessionId, file, b.methods, undefined, b.profile);
1245
+ await this.emit(p, 'activity.changed', method);
1163
1246
  return { profile: connection.profile, connectionFile: file, projectId: p.id, methods: connection.methods, transport: 'stdio-mcp', externalSessionId: b.externalSessionId };
1164
1247
  }
1165
1248
  // The person's own Claude Code or Codex, outside Rho: the same grant the operator gives, kept
@@ -1190,11 +1273,16 @@ export class RhoService {
1190
1273
  // The person at this machine talks to a conversation Rho started; Agents and narrower links
1191
1274
  // cannot start one, and an Agent started this way cannot start another.
1192
1275
  if (method.startsWith('agent.sessions.')) {
1193
- requireThat(principal.id === localBrowserId, 'unauthorized', 'External conversation history belongs to the person at this machine.');
1194
- const agent = this.plugins.need('agent');
1276
+ requireThat(principal.id === localBrowserId, 'unauthorized', 'Agent session history belongs to the person at this machine.');
1195
1277
  if (method === 'agent.sessions.list')
1196
- return agent.listExternalSessions(p.root);
1197
- return agent.readExternalSession(p.root, b);
1278
+ return this.agentSessions(p);
1279
+ const listed = (await this.agentSessions(p)).sessions.find(s => s.id === b.session);
1280
+ requireThat(listed, 'not_found', 'That Agent session is not in this project.');
1281
+ const read = listed.producer === 'claude-code' || listed.producer === 'codex'
1282
+ ? await (await import('@rho-workspace/plugin-agent/native-sessions')).readNativeSession(p.root, b)
1283
+ : this.plugins.need('agent').readExternalSession(p.root, b);
1284
+ requireThat(read, 'not_found', 'That Agent session is no longer in this project.');
1285
+ return { ...read, session: { ...read.session, ...listed } };
1198
1286
  }
1199
1287
  if (method === 'project.work.read') {
1200
1288
  requireThat(principal.id === localBrowserId, 'scope', 'Project work is available to the person at this machine.');
@@ -1235,16 +1323,35 @@ export class RhoService {
1235
1323
  if (method === 'conversation.settings.read')
1236
1324
  return this.conversations.settings();
1237
1325
  this.plugins.need('agent');
1326
+ if (method === 'conversation.start' && b.continue) {
1327
+ const listed = (await this.agentSessions(p)).sessions.find(s => s.id === b.continue.session);
1328
+ requireThat(listed, 'not_found', 'That Agent session is not in this project. Nothing was sent.');
1329
+ requireThat(listed.continueWith, 'condition', listed.producer === 'dsh'
1330
+ ? 'Rho continues DeepSeek Harness sessions through Harness\'s ACP command. Add DeepSeek Harness under Agents in Rho, or continue in Harness. Nothing was sent.'
1331
+ : 'Rho cannot continue this Agent\'s sessions. Nothing was sent.');
1332
+ requireThat(!listed.conversationId, 'conflict', 'This session already continues as a conversation here. Open it to send your message.');
1333
+ requireThat(!b.continue.copy || listed.producer !== 'dsh', 'condition', 'DeepSeek Harness cannot copy a session. Continue it in place once it is not answering. Nothing was sent.');
1334
+ requireThat(!listed.open || listed.producer !== 'dsh', 'condition', 'DeepSeek Harness is answering in this session now. Continue it once that turn ends. Nothing was sent.', { heldBy: 'DeepSeek Harness' });
1335
+ return this.conversations.start(p, principal.id, { ...b, continue: { producer: listed.producer, sessionId: listed.sessionId, title: listed.title, where: listed.where ?? (listed.producer === 'dsh' ? 'DeepSeek Harness' : 'another window'), copy: !!b.continue.copy, backend: listed.continueWith } });
1336
+ }
1238
1337
  if (method === 'conversation.start')
1239
1338
  return this.conversations.start(p, principal.id, b);
1240
- if (method === 'conversation.send')
1339
+ if (method === 'conversation.send') {
1340
+ // A Harness session reports its own turns through its adapter; Rho does not write into one it is answering.
1341
+ const listed = (await this.agentSessions(p)).sessions.find(s => s.conversationId === b.conversationId && s.producer === 'dsh');
1342
+ requireThat(!listed?.open, 'condition', 'DeepSeek Harness is answering in this session now. Send once that turn ends. Nothing was sent.', { heldBy: 'DeepSeek Harness' });
1241
1343
  return this.conversations.send(p, principal.id, b);
1344
+ }
1242
1345
  if (method === 'conversation.configure')
1243
1346
  return this.conversations.configure(p, principal.id, b);
1244
1347
  if (method === 'conversation.settings.update')
1245
1348
  return this.conversations.updateSettings(b);
1246
1349
  if (method === 'conversation.settings.check')
1247
1350
  return this.conversations.check(p, b);
1351
+ if (method === 'conversation.agents.add')
1352
+ return this.conversations.addAgent(p, b);
1353
+ if (method === 'conversation.agents.remove')
1354
+ return this.conversations.removeAgent(b);
1248
1355
  }
1249
1356
  const capabilities = new Capabilities(this.plugins, p);
1250
1357
  // The person runs and stops Console commands with their own shell; anyone holding the read
@@ -1304,6 +1411,21 @@ export class RhoService {
1304
1411
  if (method === 'kernel.server.skill')
1305
1412
  return host.serverSkill(b.server, b.name);
1306
1413
  }
1414
+ // A plot the live R session drew, with its marks bound to their rows. The person and their
1415
+ // Agent read it and keep the marks they point to; a kept selection reaches open pages.
1416
+ if (method.startsWith('figure.')) {
1417
+ await capabilities.assertAvailable(method, principal);
1418
+ const figures = this.figuresFor(p);
1419
+ const ref = { cellId: b.cellId, plot: b.plot, path: b.path, sha256: b.sha256 };
1420
+ if (method === 'figure.read')
1421
+ return figures.read({ ...ref, form: b.form, marks: b.marks, selection: b.selection, where: b.where, limit: b.limit });
1422
+ if (method === 'figure.select') {
1423
+ const result = await figures.select({ ...ref, marks: b.marks, note: b.note, changeId: b.changeId }, principal.id);
1424
+ const kept = result.selection;
1425
+ await this.emit(p, 'figure.changed', kept.sha256 ? 'file:' + kept.sha256 : kept.cellId + ':' + kept.plot, principal.id);
1426
+ return result;
1427
+ }
1428
+ }
1307
1429
  // How work is done in the project: the person and their Agent read its skills, and record and
1308
1430
  // undo changes to them, each kept with who made it, why and from what.
1309
1431
  if (method.startsWith('skills.')) {
@@ -1487,8 +1609,12 @@ export class RhoService {
1487
1609
  return owners.need('charts').plot(b.resource, b.x, b.y, b.maxPoints);
1488
1610
  if (method === 'data.compare')
1489
1611
  return owners.need('tables').compare(b.left, b.right, b.options);
1490
- if (['composition.apply', 'composition.undo', 'view.protection.set'].includes(method)) {
1491
- const result = await workspace.apply(b, principal.id, method === 'composition.undo' ? 'undo' : method === 'view.protection.set' ? 'protect' : 'apply');
1612
+ if (['composition.apply', 'composition.undo', 'composition.delete', 'view.protection.set'].includes(method)) {
1613
+ if (method === 'composition.delete' && (await routing.read())?.compositionId === b.compositionId) {
1614
+ const retained = await workspace.read(b.compositionId);
1615
+ requireThat(retained.receipts?.some((r) => r.changeId === b.changeId), 'condition', 'An active task uses this layout.');
1616
+ }
1617
+ const result = await workspace.apply(b, principal.id, method === 'composition.undo' ? 'undo' : method === 'composition.delete' ? 'delete' : method === 'view.protection.set' ? 'protect' : 'apply');
1492
1618
  const route = await routing.read();
1493
1619
  if (route) {
1494
1620
  try {
package/dist/state.d.ts CHANGED
@@ -40,6 +40,10 @@ export interface Principal {
40
40
  label?: string;
41
41
  conversationId?: string;
42
42
  added?: AddedTo;
43
+ client?: {
44
+ name: string;
45
+ version?: string;
46
+ };
43
47
  }
44
48
  export declare function lease(directory: string): Promise<() => Promise<void>>;
45
49
  export declare class State {
@@ -55,6 +59,10 @@ export declare class State {
55
59
  private persistPrincipals;
56
60
  addPrincipal(principal: Principal): Promise<void>;
57
61
  removePrincipal(id: string): Promise<void>;
62
+ markClient(id: string, client: {
63
+ name: string;
64
+ version?: string;
65
+ }): Promise<void>;
58
66
  markAdded(id: string, added: AddedTo): Promise<void>;
59
67
  revokeBrowser(id: string): Promise<{
60
68
  sessionId: string;
package/dist/state.js CHANGED
@@ -90,6 +90,14 @@ export class State {
90
90
  async addPrincipal(principal) { this.principals.push(principal); await this.persistPrincipals(); }
91
91
  async removePrincipal(id) { this.principals = this.principals.filter(p => p.id !== id); await this.persistPrincipals(); }
92
92
  // Which Agent outside Rho a grant was added to, kept with the grant it serves.
93
+ // The MCP client that last used a grant, as it named itself; kept with the grant it used.
94
+ async markClient(id, client) {
95
+ const principal = this.principals.find(p => p.id === id);
96
+ if (!principal || (principal.client?.name === client.name && principal.client?.version === client.version))
97
+ return;
98
+ principal.client = client;
99
+ await this.persistPrincipals();
100
+ }
93
101
  async markAdded(id, added) { const principal = this.principals.find(p => p.id === id); if (principal) {
94
102
  principal.added = added;
95
103
  await this.persistPrincipals();
@@ -49,7 +49,7 @@ export function studioFiles(graph) {
49
49
  take(item.node, role, item.role === 'primary');
50
50
  }
51
51
  }
52
- return [...found.values()].sort((a, b) => rank[a.role] - rank[b.role] || Number(b.lead) - Number(a.lead) || a.first - b.first || a.path.localeCompare(b.path));
52
+ return [...found.values()].sort((a, b) => rank[a.role] - rank[b.role] || (a.role === 'input' && b.role === 'input' ? Number(b.category === 'data') - Number(a.category === 'data') : 0) || Number(b.lead) - Number(a.lead) || a.first - b.first || a.path.localeCompare(b.path));
53
53
  }
54
54
  // The files as they stood at a moment: those the work had by then, each with the versions kept
55
55
  // by then. Null is now.
@@ -76,10 +76,10 @@ export function studioTree(views, commands, session = null) {
76
76
  return id;
77
77
  };
78
78
  const input = region(studioIds.input, of('input').map(v => v.node)), code = region(studioIds.code, of('code').map(v => v.node));
79
- const run = region(studioIds.commands, [commands, session].filter((n) => !!n));
79
+ const run = region(studioIds.commands, [session, commands].filter((n) => !!n));
80
80
  const made = [...of('result'), ...of('report')];
81
81
  const figures = region(studioIds.figures, made.filter(v => v.figure).map(v => v.node)), result = region(studioIds.result, made.filter(v => !v.figure).map(v => v.node));
82
- const leftId = column(studioIds.left, [input ? { id: input, weight: 1 } : null, code ? { id: code, weight: 1.8 } : null, run ? { id: run, weight: 1.2 } : null]);
82
+ const leftId = column(studioIds.left, [input ? { id: input, weight: 1.4 } : null, code ? { id: code, weight: 1.2 } : null, run ? { id: run, weight: 1.4 } : null]);
83
83
  const rightId = column(studioIds.right, [figures ? { id: figures, weight: 1.25 } : null, result ? { id: result, weight: 1 } : null]);
84
84
  if (leftId && rightId) {
85
85
  nodes.push(split(studioIds.root, 'horizontal', [{ id: leftId, weight: 1 }, { id: rightId, weight: 1.3 }]));
@@ -98,4 +98,4 @@ export declare function counts(map: TaskMapState): {
98
98
  waiting: number;
99
99
  done: number;
100
100
  };
101
- export declare function authorLabel(author: string, principalId?: string): "You" | "Someone in another tab" | "Rho" | "The Agent";
101
+ export declare function authorLabel(author: string, principalId?: string): "Rho" | "You" | "Someone in another tab" | "The Agent";
@@ -8,6 +8,7 @@ export interface LaneOwner {
8
8
  principal: string | null;
9
9
  title: string;
10
10
  agent: string;
11
+ eventKeys?: string[];
11
12
  }
12
13
  export type Who = 'person' | 'conversation' | 'other';
13
14
  export interface EventMark {
@@ -6,13 +6,20 @@ export const personPrincipal = 'browser:local';
6
6
  export const personLane = 'person', otherLane = 'other';
7
7
  export function eventMarks(events, owners, now) {
8
8
  const byPrincipal = new Map(owners.flatMap(o => o.principal ? [[o.principal, o]] : []));
9
+ const byEvent = new Map();
10
+ for (const owner of owners)
11
+ for (const key of owner.eventKeys ?? []) {
12
+ const previous = byEvent.get(key);
13
+ byEvent.set(key, previous === undefined || previous?.conversationId === owner.conversationId ? owner : null);
14
+ }
9
15
  return events.flatMap(e => {
10
16
  const at = Date.parse(e.startedAt);
11
17
  if (!Number.isFinite(at))
12
18
  return [];
13
19
  const ended = e.endedAt ? Date.parse(e.endedAt) : NaN;
14
20
  const end = Math.max(at, Number.isFinite(ended) ? ended : e.outcome === 'running' ? now : at);
15
- const owner = e.by === personPrincipal ? null : byPrincipal.get(e.by);
21
+ const key = e.source + ':' + e.id;
22
+ const owner = e.by === personPrincipal ? null : byEvent.has(key) ? byEvent.get(key) : byPrincipal.get(e.by);
16
23
  const who = e.by === personPrincipal ? 'person' : owner ? 'conversation' : 'other';
17
24
  return [{ key: e.source + ':' + e.id, at, end, lane: who === 'person' ? personLane : owner ? owner.conversationId : otherLane,
18
25
  laneTitle: who === 'person' ? 'You' : owner ? owner.title : 'Outside Rho', who, ...(owner ? { agent: owner.agent } : {}),