@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
@@ -1,11 +1,12 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
3
  import { statements, unfinished } from './statements.js';
4
4
  import { cellRelations } from './lineage.js';
5
5
  import { CellScriptFlow } from './ui-flow.js';
6
6
  import { Icon } from './ui-icons.js';
7
7
  import { PlaceChip, usePlaces, whereOf } from './ui-servers.js';
8
8
  import { takeCell } from './ui-notebook-target.js';
9
+ import { scopedCells } from './session-scope.js';
9
10
  import { call, refused, agentName, clock, durationText, useShell, withShell, versionOf } from './ui-base.js';
10
11
  export { versionOf };
11
12
  export const languages = ['r', 'python'];
@@ -69,13 +70,30 @@ export function usePlot(client, projectId, cellId, plot) {
69
70
  }, [client, projectId, cellId, plot]);
70
71
  return { src, problem };
71
72
  }
73
+ // What a figure on this page needs from it: who ran what, and the page's event cursor.
74
+ const FigureFacts = createContext({});
75
+ function WithFacts({ snapshot, children }) {
76
+ const who = useAuthors(snapshot), cursor = snapshot.cursor;
77
+ return _jsx(FigureFacts.Provider, { value: useMemo(() => ({ who, cursor }), [who, cursor]), children: children });
78
+ }
79
+ // A plot an extension described is drawn by the page's figure, with the picture standing in until
80
+ // then; one it could not describe says why under its picture.
81
+ function useFigure(client, projectId, cell, output, label, picture, fit = false) {
82
+ const { figure } = useShell(), facts = useContext(FigureFacts);
83
+ if (!output.figure || !figure)
84
+ return null;
85
+ return figure({ client, projectId, cellId: cell.cellId, plot: output.plot, width: output.width || 1008, height: output.height || 720, label, name: `${languageName[cell.language]} cell ${cell.number}, plot ${output.plot}`, fit, cursor: facts.cursor, fallback: picture,
86
+ author: facts.who && (by => ({ name: facts.who(by).name, mine: by === me })) });
87
+ }
72
88
  function Plot({ client, projectId, cell, output, label }) {
73
89
  const { src, problem } = usePlot(client, projectId, cell.cellId, output.plot);
74
90
  // Plots are drawn at twice the size they are shown at, for sharp lines on dense screens.
75
91
  const width = Math.round((output.width || 1008) / 2), height = Math.round((output.height || 720) / 2);
92
+ const picture = _jsx("figure", { className: "nb-plot", style: { maxWidth: width }, children: src ? _jsx("img", { src: src, width: width, height: height, alt: label }) : _jsx("div", { className: "nb-plot-wait", style: { aspectRatio: `${width} / ${height}` }, "aria-label": "Reading the plot" }) });
93
+ const described = useFigure(client, projectId, cell, output, label, picture);
76
94
  if (problem)
77
95
  return _jsxs("p", { className: "nb-note", children: ["This plot could not be read. ", problem] });
78
- return _jsx("figure", { className: "nb-plot", style: { maxWidth: width }, children: src ? _jsx("img", { src: src, width: width, height: height, alt: label }) : _jsx("div", { className: "nb-plot-wait", style: { aspectRatio: `${width} / ${height}` }, "aria-label": "Reading the plot" }) });
96
+ return _jsxs(_Fragment, { children: [described ?? picture, output.figureProblem && _jsxs("p", { className: "nb-note", children: ["Its marks are not selectable: ", output.figureProblem] })] });
79
97
  }
80
98
  export function Code({ code, language }) {
81
99
  const lines = code.replace(/\n+$/, '').split('\n');
@@ -100,12 +118,23 @@ function Printed({ output }) {
100
118
  const long = output.kind !== 'error' && lines.length > notebookViewLimits.outputLines + 4;
101
119
  return _jsxs(_Fragment, { children: [_jsx("pre", { className: 'nb-out ' + output.kind, children: long && !whole ? lines.slice(0, notebookViewLimits.outputLines).join('\n') : lines.join('\n') }), long && _jsx("button", { className: "text-button nb-more nb-out-more", onClick: () => setWhole(!whole), children: whole ? 'Show less' : `Show all ${lines.length} lines` })] });
102
120
  }
121
+ function Saved({ client, projectId, cell, saved }) {
122
+ const { figure, openPath } = useShell(), facts = useContext(FigureFacts);
123
+ const name = saved.path.split('/').at(-1);
124
+ const head = _jsxs("p", { className: "nb-saved", children: [_jsx("span", { children: "Saved" }), openPath ? _jsx("button", { className: "text-button", title: 'Show ' + saved.path, onClick: () => openPath(saved.path), children: saved.path }) : _jsx("code", { children: saved.path }), saved.figure ? _jsxs("span", { className: "nb-saved-what", children: [saved.figure.bound.toLocaleString(), " ", saved.figure.bound === 1 ? 'mark' : 'marks', " bound to their rows"] })
125
+ : saved.figureProblem ? _jsxs("span", { className: "nb-saved-what", children: ["kept as a picture: ", saved.figureProblem] }) : null] });
126
+ if (!saved.figure || !figure)
127
+ return head;
128
+ return _jsxs(_Fragment, { children: [head, figure({ client, projectId, file: { path: saved.path, sha256: saved.sha256 }, width: 1008, label: `${name}, saved by ${cell.purpose ?? 'this cell'}`, name: `${name} (${languageName[cell.language]} cell ${cell.number})`,
129
+ cursor: facts.cursor, fallback: _jsx("div", { className: "nb-plot-wait", style: { maxWidth: 504, aspectRatio: '7 / 5' }, "aria-label": "Reading the figure" }),
130
+ author: facts.who && (by => ({ name: facts.who(by).name, mine: by === me })) })] });
131
+ }
103
132
  function Outputs({ cell, client, projectId }) {
104
133
  let plots = 0;
105
134
  const purpose = cell.purpose ?? 'this cell';
106
135
  return _jsxs(_Fragment, { children: [cell.outputs.map((output, i) => output.kind === 'plot'
107
136
  ? _jsx(Plot, { client: client, projectId: projectId, cell: cell, output: output, label: `Plot ${++plots} drawn by ${purpose}` }, i)
108
- : _jsx(Printed, { output: output }, i)), cell.cut && _jsxs("p", { className: "nb-note", children: ["Only the first ", sizeText(cell.kept), " of the ", sizeText(cell.bytes), " it printed were kept."] })] });
137
+ : _jsx(Printed, { output: output }, i)), (cell.saved ?? []).map(saved => _jsx(Saved, { client: client, projectId: projectId, cell: cell, saved: saved }, saved.number)), cell.cut && _jsxs("p", { className: "nb-note", children: ["Only the first ", sizeText(cell.kept), " of the ", sizeText(cell.bytes), " it printed were kept."] })] });
109
138
  }
110
139
  const sizeText = (bytes) => bytes < 1024 ? bytes + ' bytes' : bytes < 1024 * 1024 ? Math.round(bytes / 1024) + ' KB' : (bytes / 1024 / 1024).toFixed(1) + ' MB';
111
140
  function Ending({ cell, now }) {
@@ -286,7 +315,7 @@ function ConsoleCell({ cell, author, who, client, projectId, now, plotsBeside, p
286
315
  const interrupted = cell.interrupt ? `Interrupted by ${who(cell.interrupt.by).name === 'You' ? 'you' : who(cell.interrupt.by).name}` : null;
287
316
  return _jsxs("div", { className: 'nbc-cell ' + cell.state + ' ' + cell.language, id: 'cell-' + cell.cellId, children: [author.name !== 'You' && _jsxs("p", { className: "nbc-who", children: ["# ", author.conversationId ? _jsx("button", { className: "text-button", title: author.title ? 'Open ' + author.title : undefined, onClick: () => onConversation(author.conversationId), children: author.name }) : author.name, cell.purpose ? ' · ' + cell.purpose : ''] }), _jsx(PromptCode, { code: cell.code, language: cell.language }), cell.outputs.map((output, i) => output.kind === 'plot'
288
317
  ? _jsx(ConsolePlot, { client: client, projectId: projectId, cell: cell, output: output, number: plotNumber(cell.cellId, output.plot), beside: plotsBeside }, i)
289
- : _jsx("pre", { className: 'nbc-out ' + output.kind, children: (output.text ?? '').replace(/\n$/, '') }, i)), cell.cut && _jsxs("p", { className: "nbc-quiet", children: ["Only the first ", sizeText(cell.kept), " of the ", sizeText(cell.bytes), " it printed were kept."] }), cell.state === 'running' && _jsxs("p", { className: "nbc-state running", children: [_jsx("span", { className: "pulse-dot", "aria-hidden": "true" }), "Running ", durationText(now - Date.parse(cell.startedAt ?? cell.queuedAt))] }), cell.state === 'queued' && _jsx("p", { className: "nbc-state", children: "Waiting for the cell before it" }), (interrupted || cell.state === 'not-run' || cell.state === 'unknown' || (cell.problem && cell.state !== 'error' && cell.state !== 'interrupted')) &&
318
+ : _jsx("pre", { className: 'nbc-out ' + output.kind, children: (output.text ?? '').replace(/\n$/, '') }, i)), (cell.saved ?? []).map(saved => _jsxs("p", { className: "nbc-quiet", children: ["# Saved ", saved.path, saved.figure ? ` · ${saved.figure.bound.toLocaleString()} marks bound` : saved.figureProblem ? ` · kept as a picture: ${saved.figureProblem}` : ''] }, saved.number)), cell.cut && _jsxs("p", { className: "nbc-quiet", children: ["Only the first ", sizeText(cell.kept), " of the ", sizeText(cell.bytes), " it printed were kept."] }), cell.state === 'running' && _jsxs("p", { className: "nbc-state running", children: [_jsx("span", { className: "pulse-dot", "aria-hidden": "true" }), "Running ", durationText(now - Date.parse(cell.startedAt ?? cell.queuedAt))] }), cell.state === 'queued' && _jsx("p", { className: "nbc-state", children: "Waiting for the cell before it" }), (interrupted || cell.state === 'not-run' || cell.state === 'unknown' || (cell.problem && cell.state !== 'error' && cell.state !== 'interrupted')) &&
290
319
  _jsx("p", { className: "nbc-state", children: interrupted ?? (cell.state === 'not-run' ? 'Not run' : cell.state === 'unknown' ? 'Outcome unknown' : cell.problem) })] });
291
320
  }
292
321
  function ConsoleMark({ session, edge, who }) {
@@ -422,26 +451,19 @@ export function SessionPane({ client, snapshot, prepared, onTaken, onConversatio
422
451
  observer.observe(element);
423
452
  return () => observer.disconnect();
424
453
  }, []);
425
- return withShell(shell, _jsx("div", { className: "nb-view", ref: box, children: _jsx(NotebookBody, { client: client, snapshot: snapshot, onConversation: onConversation ?? (() => { }), compact: !wide, inView: true, shown: shown, prepared: prepared, onTaken: onTaken, onObject: onObject, beside: beside, form: form, scope: scope }) }));
454
+ return withShell(shell, _jsx(WithFacts, { snapshot: snapshot, children: _jsx("div", { className: "nb-view", ref: box, children: _jsx(NotebookBody, { client: client, snapshot: snapshot, onConversation: onConversation ?? (() => { }), compact: !wide, inView: true, shown: shown, prepared: prepared, onTaken: onTaken, onObject: onObject, beside: beside, form: form, scope: scope }) }) }));
426
455
  }
427
456
  // The Notebook as a page of its own, or inside a view. The page that shows it may give it the
428
457
  // application's code highlighting.
429
458
  export function NotebookPage({ shell, ...props }) {
430
- return withShell(shell, _jsx(NotebookBody, { ...props }));
459
+ return withShell(shell, _jsx(WithFacts, { snapshot: props.snapshot, children: _jsx(NotebookBody, { ...props }) }));
431
460
  }
432
461
  function NotebookBody({ client, snapshot, onConversation, onObject, scope, compact = false, inView = false, shown = 0, prepared, onTaken, beside = {}, form = 'notebook' }) {
433
462
  const projectId = snapshot.project.id;
434
463
  const { read, off, problem, again } = useKernel(client, projectId, snapshot.cursor);
435
464
  const { places, setPlaces } = usePlaces(client, projectId, snapshot.cursor);
436
465
  const who = useAuthors(snapshot);
437
- const cells = useMemo(() => {
438
- const all = read?.cells ?? [];
439
- if (!scope)
440
- return all;
441
- const theirs = new Set(all.filter(c => c.by === scope.principal).map(c => c.sessionId));
442
- return all.filter(c => (theirs.has(c.sessionId) || (c.by === me && scope.since !== null && Date.parse(c.queuedAt) >= scope.since))
443
- && (scope.at === null || Date.parse(c.queuedAt) <= scope.at));
444
- }, [read, scope?.principal, scope?.since, scope?.at]);
466
+ const cells = useMemo(() => scopedCells(read?.cells ?? [], scope), [read, scope?.principal, scope?.since, scope?.at, scope?.cellIds]);
445
467
  const latest = useMemo(() => {
446
468
  const out = {};
447
469
  for (const s of read?.sessions ?? [])
@@ -679,7 +701,10 @@ export function VariablesPane({ client, snapshot, onObject }) {
679
701
  // the ones before it to step back through. A new plot comes forward as it is drawn. Plots of an
680
702
  // ended session stay with their cells in the Notebook.
681
703
  export const plotsPaneLimits = { plots: 24 };
682
- export function PlotsPane({ client, snapshot }) {
704
+ export function PlotsPane({ client, snapshot, shell }) {
705
+ return withShell(shell, _jsx(WithFacts, { snapshot: snapshot, children: _jsx(Plots, { client: client, snapshot: snapshot }) }));
706
+ }
707
+ function Plots({ client, snapshot }) {
683
708
  const projectId = snapshot.project.id;
684
709
  const { read, off, problem } = useKernel(client, projectId, snapshot.cursor);
685
710
  const who = useAuthors(snapshot);
@@ -711,9 +736,11 @@ export function PlotsPane({ client, snapshot }) {
711
736
  }
712
737
  function PlotFit({ client, projectId, cell, output, label }) {
713
738
  const { src, problem } = usePlot(client, projectId, cell.cellId, output.plot);
739
+ const picture = _jsx("figure", { className: "nb-plots-fit", children: src ? _jsx("img", { src: src, alt: label }) : _jsx("span", { className: "nb-plot-wait", "aria-label": "Reading the plot" }) });
740
+ const described = useFigure(client, projectId, cell, output, label, picture, true);
714
741
  if (problem)
715
742
  return _jsxs("p", { className: "nb-note", children: ["This plot could not be read. ", problem] });
716
- return _jsx("figure", { className: "nb-plots-fit", children: src ? _jsx("img", { src: src, alt: label }) : _jsx("span", { className: "nb-plot-wait", "aria-label": "Reading the plot" }) });
743
+ return described ? _jsx("div", { className: "nb-plots-figure", children: described }) : picture;
717
744
  }
718
745
  function PlotThumb({ client, projectId, cell, output, current, label, onShow }) {
719
746
  const { src } = usePlot(client, projectId, cell.cellId, output.plot);
@@ -40,6 +40,10 @@ const styles = `/* The Notebook: cells in the order they ran beside the live ses
40
40
  .nb-plot img { display: block; width: 100%; height: auto; border: 1px solid var(--color-hairline); border-radius: 6px; background: #fff; }
41
41
  .nb-plot-wait { display: block; width: 100%; border-radius: 6px; background: var(--color-well); }
42
42
  .nb-note { color: var(--color-meta); font-size: 12px; }
43
+ .nb-saved { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin: 6px 0; font-size: 12px; color: var(--color-meta); }
44
+ .nb-saved > span:first-child { font-weight: 600; color: var(--color-ink); }
45
+ .nb-saved .text-button { font-family: var(--font-mono, ui-monospace, monospace); font-size: 12px; overflow-wrap: anywhere; }
46
+ .nb-saved-what { overflow-wrap: anywhere; }
43
47
  .nb-body > .nb-note { padding-left: 12px; }
44
48
  .nb-problem { padding-left: 12px; color: var(--color-amber-text); font-size: 12.5px; }
45
49
  .nb-actions { display: flex; gap: 16px; padding-left: 12px; }
@@ -188,6 +192,8 @@ button.nb-cell-link:hover { background: var(--color-hover); text-decoration: und
188
192
  .nb-plots-fit { flex: 1; display: flex; align-items: center; justify-content: center; min-height: 0; margin: 0; }
189
193
  .nb-plots-fit img { max-width: 100%; max-height: 100%; object-fit: contain; border: 1px solid var(--color-hairline); border-radius: 6px; background: #fff; }
190
194
  .nb-plots-fit .nb-plot-wait { width: min(100%, 504px); aspect-ratio: 7 / 5; }
195
+ .nb-plots-figure { flex: 1; min-height: 0; display: flex; flex-direction: column; }
196
+ .nb-plots-figure > .fig { flex: 1; min-height: 0; }
191
197
  .nb-plots-strip { display: flex; flex: none; gap: 6px; overflow-x: auto; padding: 2px; }
192
198
  .nb-plots-thumb { flex: none; width: 72px; min-height: 0; padding: 2px; border: 1px solid var(--color-hairline); border-radius: 6px; background: var(--color-surface); }
193
199
  .nb-plots-thumb[aria-current="true"] { border-color: var(--color-accent); box-shadow: 0 0 0 1px var(--color-accent); }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rho-workspace/plugin-kernel",
3
- "version": "0.5.5",
3
+ "version": "0.5.13",
4
4
  "type": "module",
5
5
  "description": "Rho live session plugin: an R and a Python session the Agent and the person run code in, kept with each cell's output, plots and the session's variables",
6
6
  "license": "AGPL-3.0-only",
@@ -161,7 +161,98 @@ call_text <- function(condition) {
161
161
  # that draws several keeps each; a cell reports the pages it drew or changed, drawn to files at
162
162
  # its end. A device the code opens itself is its own.
163
163
  device <- NULL; seen <- NULL; plots <- list()
164
+ # Plugins can describe what a page draws (RHO_KERNEL_EXTENSIONS names their R files). Each file is
165
+ # sourced into an environment of its own; page_object() names the object a page starts drawing,
166
+ # and describe_page() writes a record beside the page's picture. One that fails is left out, and a
167
+ # page whose object no extension describes stays a picture.
168
+ extensions <- list()
169
+ for (file in strsplit(Sys.getenv("RHO_KERNEL_EXTENSIONS"), .Platform$path.sep, fixed = TRUE)[[1]]) {
170
+ if (!nzchar(file)) next
171
+ env <- new.env(parent = baseenv())
172
+ if (!inherits(tryCatch(sys.source(file, envir = env), error = function(e) e), "error")) extensions[[length(extensions) + 1]] <- env
173
+ }
174
+ page_object <- NULL
175
+ # Pages the code saved to files of its own (ggsave): an extension names the plot, the file and the
176
+ # size, and the page is described when the cell ends, once the file is written. A file saved twice
177
+ # keeps its last page.
178
+ saving <- list()
179
+ absolute <- function(file) if (grepl("^(/|~|[A-Za-z]:[/\\\\])", file)) path.expand(file) else file.path(getwd(), file)
180
+ # A device the code opens on a file (png(), pdf(), svg() and the others grDevices names) is noted
181
+ # with its file. Each page drawn there is recorded as it is drawn: before the next page starts,
182
+ # after each top-level expression and before dev.off() closes it. When it closes, its last page (or
183
+ # each page, for a file name numbered with %d) is described as a figure of its file, as a page
184
+ # ggsave() saved is. The runner's own devices are not noted. A device opened through a function a
185
+ # package imported before the session started is not seen; its file stays a picture.
186
+ drawing <- list(); closed <- list(); own_devices <- FALSE
187
+ numbered <- function(file) grepl("%[#0 +-]*[0-9]*d", file)
188
+ file_snapshot <- function(d) {
189
+ key <- as.character(d); f <- drawing[[key]]
190
+ if (is.null(f) || !length(f$pages) || !(d %in% grDevices::dev.list())) return()
191
+ current <- grDevices::dev.cur(); if (current != d) grDevices::dev.set(d)
192
+ p <- tryCatch(grDevices::recordPlot(), error = function(e) NULL); size <- grDevices::dev.size("in")
193
+ if (current != d) grDevices::dev.set(current)
194
+ if (!is.null(p) && length(drawn(p[[1]]))) drawing[[key]]$pages[[length(f$pages)]] <<- c(f$pages[[length(f$pages)]][c("object")], list(recorded = p, width = size[1], height = size[2]))
195
+ }
196
+ file_page <- function(d, grid) {
197
+ key <- as.character(d)
198
+ if (is.null(drawing[[key]])) return()
199
+ file_snapshot(d)
200
+ grDevices::dev.control("enable")
201
+ # A file that is not numbered keeps only its last page: earlier ones are counted, not kept.
202
+ n <- length(drawing[[key]]$pages)
203
+ if (n && !numbered(drawing[[key]]$file)) drawing[[key]]$pages[[n]] <<- list(object = NULL)
204
+ object <- NULL
205
+ if (grid) for (env in extensions) if (is.function(env$page_object)) {
206
+ found <- tryCatch(env$page_object(), error = function(e) NULL)
207
+ if (!is.null(found)) { object <- list(env = env, object = found); break }
208
+ }
209
+ drawing[[key]]$pages[[n + 1L]] <<- list(object = object)
210
+ }
211
+ device_opening <- function(env) assign(".rho_devices", grDevices::dev.list(), envir = env)
212
+ device_opened <- function(env) tryCatch({
213
+ d <- grDevices::dev.cur(); before <- get0(".rho_devices", envir = env, inherits = FALSE)
214
+ if (d == 1L || d %in% before) return(invisible())
215
+ # A device number is used again once its device is closed; the runner's own device is never noted.
216
+ drawing[[as.character(d)]] <<- NULL
217
+ file <- get0("filename", envir = env, inherits = FALSE) %||% get0("file", envir = env, inherits = FALSE)
218
+ if (own_devices || identical(d, device) || !is.character(file) || length(file) != 1L || !nzchar(file)) return(invisible())
219
+ drawing[[as.character(d)]] <<- list(file = absolute(file), pages = list())
220
+ invisible()
221
+ }, error = function(e) invisible())
222
+ device_closing <- function(which) tryCatch({
223
+ d <- as.integer(which); key <- as.character(d)
224
+ if (length(d) != 1L || is.na(d) || is.null(drawing[[key]])) return(invisible())
225
+ file_snapshot(d)
226
+ closed[[length(closed) + 1L]] <<- drawing[[key]]; drawing[[key]] <<- NULL
227
+ invisible()
228
+ }, error = function(e) invisible())
229
+ invisible(utils::capture.output(suppressMessages(tryCatch(
230
+ for (where in list(asNamespace("grDevices"), as.environment("package:grDevices"))) {
231
+ for (f in intersect(c("png", "jpeg", "tiff", "bmp", "svg", "pdf", "cairo_pdf"), ls(where)))
232
+ trace(f, tracer = substitute(F(environment()), list(F = device_opening)), exit = substitute(F(environment()), list(F = device_opened)), where = where, print = FALSE)
233
+ trace("dev.off", tracer = substitute(F(which), list(F = device_closing)), where = where, print = FALSE)
234
+ }, error = function(e) NULL))))
235
+ started_page <- function(grid) {
236
+ current <- grDevices::dev.cur()
237
+ if (!is.null(device) && current != device) {
238
+ if (grid) for (env in extensions) if (is.function(env$saved_page)) {
239
+ found <- tryCatch(env$saved_page(), error = function(e) NULL)
240
+ if (!is.null(found)) { found$env <- env; saving[[absolute(found$file)]] <<- found; drawing[[as.character(current)]] <<- NULL; return() }
241
+ }
242
+ file_page(current, grid)
243
+ return()
244
+ }
245
+ if (is.null(device) || grDevices::dev.cur() != device) return()
246
+ page_object <<- NULL
247
+ if (!grid) return()
248
+ for (env in extensions) if (is.function(env$page_object)) {
249
+ found <- tryCatch(env$page_object(), error = function(e) NULL)
250
+ if (!is.null(found)) { page_object <<- list(env = env, object = found); return() }
251
+ }
252
+ }
164
253
  recording <- function() {
254
+ # A file the code opened and has not closed stays where it draws, as in the R console.
255
+ if (!is.null(drawing[[as.character(grDevices::dev.cur())]])) return()
165
256
  if (!is.null(device) && device %in% grDevices::dev.list()) { grDevices::dev.set(device); return() }
166
257
  grDevices::pdf(NULL, width = 7, height = 5); grDevices::dev.control("enable")
167
258
  device <<- grDevices::dev.cur(); seen <<- NULL
@@ -171,13 +262,17 @@ snapshot <- function() {
171
262
  current <- grDevices::dev.cur(); if (current != device) grDevices::dev.set(device)
172
263
  p <- grDevices::recordPlot()
173
264
  if (current != device) grDevices::dev.set(current)
174
- if (length(drawn(p[[1]])) && !(length(seen) && same(drawn(p[[1]]), drawn(seen)))) { plots[[length(plots) + 1]] <<- p; seen <<- p[[1]] }
265
+ if (length(drawn(p[[1]])) && !(length(seen) && same(drawn(p[[1]]), drawn(seen)))) { attr(p, "rho_page") <- page_object; plots[[length(plots) + 1]] <<- p; seen <<- p[[1]] }
175
266
  }
176
- setHook("before.plot.new", function() snapshot())
177
- setHook("before.grid.newpage", function() snapshot())
267
+ setHook("before.plot.new", function() { snapshot(); started_page(FALSE) })
268
+ setHook("before.grid.newpage", function() { snapshot(); started_page(TRUE) })
178
269
 
270
+ # A page drawn on a file, ready to be described as that file: the object an extension named when
271
+ # the page started, else the page as it was recorded.
272
+ drawn_page <- function(p) list(env = p$object$env %||% Find(function(env) is.function(env$describe_page), extensions),
273
+ object = p$object$object %||% p$recorded, width = p$width, height = p$height, recorded = p$recorded, quiet = TRUE)
179
274
  run <- function(msg) {
180
- id <- msg$id; plots <<- list(); ok <- TRUE; interrupted <- FALSE
275
+ id <- msg$id; plots <<- list(); saving <<- list(); closed <<- list(); own_devices <<- FALSE; ok <- TRUE; interrupted <- FALSE
181
276
  known <- ls(globalenv()); seen <- lineage_of(msg$code); files <- project_paths(seen$strings)
182
277
  recording()
183
278
  tryCatch({
@@ -193,10 +288,13 @@ run <- function(msg) {
193
288
  warning = function(w) { say(list(id = id, kind = "warning", text = paste0("Warning", call_text(w), ": ", conditionMessage(w), "\n"))); invokeRestart("muffleWarning") },
194
289
  message = function(m) { say(list(id = id, kind = "stderr", text = conditionMessage(m))); invokeRestart("muffleMessage") })
195
290
  snapshot()
291
+ for (key in names(drawing)) file_snapshot(as.integer(key))
196
292
  if (!went) break
197
293
  }
198
294
  }, interrupt = function(e) { interrupted <<- TRUE; ok <<- FALSE; cat("\n") })
199
295
  # A session on a server draws in its own temporary folder and sends each plot with its answer.
296
+ # The devices drawn on from here are the runner's own.
297
+ own_devices <<- TRUE
200
298
  shown <- final_plots(plots); plots <<- list()
201
299
  for (i in seq_along(shown)) {
202
300
  if (i > msg$maxPlots) break
@@ -205,9 +303,51 @@ run <- function(msg) {
205
303
  tryCatch(grDevices::replayPlot(shown[[i]]), error = function(e) NULL)
206
304
  grDevices::dev.off()
207
305
  plot <- list(id = id, kind = "plot", file = file, width = 1008, height = 720)
306
+ # A page no extension named an object for (base graphics) is offered to them as it was recorded.
307
+ page <- attr(shown[[i]], "rho_page")
308
+ if (is.null(page)) for (env in extensions) if (is.function(env$describe_page)) { page <- list(env = env, object = shown[[i]]); break }
309
+ if (!is.null(page) && !isTRUE(msg$inline) && is.function(page$env$describe_page)) {
310
+ described <- tryCatch(page$env$describe_page(page$object, file, 7, 5, recorded = shown[[i]]), error = function(e) list(problem = conditionMessage(e)))
311
+ if (!is.null(described$problem)) plot$figureProblem <- as.character(described$problem)[1] else if (!is.null(described)) plot$figure <- described
312
+ }
208
313
  if (isTRUE(msg$inline)) { plot$data <- jsonlite::base64_enc(readBin(file, "raw", file.info(file)$size)); unlink(file) }
209
314
  say(plot)
210
315
  }
316
+ # A page saved to a file in the project is described at the size it was saved at; its record is
317
+ # kept with the cell's plots. A session on a server keeps none: its files are not here.
318
+ # Files the code drew to and closed, or whose device is gone, join them: a file numbered with %d
319
+ # holds each page, a PDF of several pages is not described, any other file holds its last page.
320
+ for (key in names(drawing)) if (!(as.integer(key) %in% grDevices::dev.list())) { closed[[length(closed) + 1L]] <<- drawing[[key]]; drawing[[key]] <<- NULL }
321
+ for (f in closed) {
322
+ n <- length(f$pages)
323
+ if (!n) next
324
+ if (numbered(f$file)) { for (j in seq_len(n)) if (!is.null(f$pages[[j]]$recorded)) saving[[sprintf(f$file, j)]] <<- drawn_page(f$pages[[j]]) }
325
+ else if (n > 1L && grepl("[.]pdf$", f$file, ignore.case = TRUE)) saving[[f$file]] <<- list(problem = sprintf("The file holds %d pages; only a file of one page is described.", n))
326
+ else if (!is.null(f$pages[[n]]$recorded)) saving[[f$file]] <<- drawn_page(f$pages[[n]])
327
+ }
328
+ closed <<- list()
329
+ saved <- saving; saving <<- list()
330
+ if (!isTRUE(msg$inline) && !is.null(msg$plots)) {
331
+ root <- normalizePath(getwd(), winslash = "/")
332
+ k <- 0L
333
+ for (named in names(saved)) {
334
+ if (k >= msg$maxPlots) break
335
+ page <- saved[[named]]
336
+ if (!file.exists(named)) next
337
+ full <- normalizePath(named, winslash = "/")
338
+ if (!startsWith(full, paste0(root, "/"))) next
339
+ described <- if (!is.null(page$problem)) list(problem = page$problem) else if (is.function(page$env$describe_page))
340
+ tryCatch(page$env$describe_page(page$object, full, page$width, page$height, file.path(msg$plots, paste0(id, "-s", k + 1L, ".figure.json")), recorded = page$recorded),
341
+ error = function(e) list(problem = conditionMessage(e)))
342
+ # A page drawn on a file that no extension describes (a lattice page) stays a picture without a word.
343
+ if (is.null(described) && isTRUE(page$quiet)) next
344
+ k <- k + 1L
345
+ out <- list(id = id, kind = "saved", path = substring(full, nchar(root) + 2), number = k)
346
+ if (!is.null(described$problem)) out$figureProblem <- as.character(described$problem)[1] else if (!is.null(described)) out$figure <- described
347
+ say(out)
348
+ }
349
+ }
350
+ own_devices <<- FALSE
211
351
  say(list(id = id, kind = "done", ok = ok, interrupted = interrupted, variables = variables(msg$maxVariables),
212
352
  used = I(intersect(seen$mentioned, known)), assigned = I(seen$assigned), files = touched(files)))
213
353
  }
@@ -4,7 +4,7 @@
4
4
  # that value, as a notebook does. Figures made with matplotlib are saved after each cell. An
5
5
  # interrupt (SIGINT; on Windows a Ctrl+C or Ctrl+Break on the session's console) stops the cell
6
6
  # that is running and leaves the session and its variables.
7
- import ast, base64, io, json, linecache, os, platform, re, signal, sys, tempfile, traceback, types
7
+ import ast, base64, functools, io, json, linecache, os, platform, re, signal, sys, tempfile, traceback, types
8
8
 
9
9
  root = sys.argv[1]
10
10
  os.chdir(root)
@@ -67,6 +67,27 @@ def variables(most):
67
67
  items.append({'name': name, 'type': type(value).__name__, 'size': size, 'bytes': bytes_, 'summary': summary})
68
68
  return {'items': items, 'omitted': max(0, len(names) - most)}
69
69
 
70
+ # Plugins can describe what a figure draws (RHO_KERNEL_EXTENSIONS names their Python files). Each file
71
+ # runs in a namespace of its own; describe_figure() writes a record beside the figure's picture. One
72
+ # that fails to load is left out, and a figure no extension describes stays a picture.
73
+ extensions = []
74
+ for file in os.environ.get('RHO_KERNEL_EXTENSIONS', '').split(os.pathsep):
75
+ if not file: continue
76
+ try:
77
+ with open(file, encoding='utf-8') as f: scope = {'__name__': 'rho_extension', '__file__': file}; exec(compile(f.read(), file, 'exec'), scope)
78
+ if callable(scope.get('describe_figure')): extensions.append(scope)
79
+ except Exception: pass
80
+
81
+ # The names the running cell's code assigns, then those it reads: a describer compares those tables
82
+ # first, a table the cell made before one it read.
83
+ cell_names = []
84
+ def describe(figure, into, **options):
85
+ for scope in extensions:
86
+ try: out = scope['describe_figure'](figure, into, namespace, prefer=cell_names, **options)
87
+ except Exception as error: return {'problem': f'{type(error).__name__}: {error}'}
88
+ if out is not None: return out
89
+ return None
90
+
70
91
  # A session on a server draws in its own temporary folder and sends each figure with its answer.
71
92
  def save_figures(cell, folder, most, inline=False):
72
93
  plt = sys.modules.get('matplotlib.pyplot')
@@ -81,9 +102,85 @@ def save_figures(cell, folder, most, inline=False):
81
102
  if inline:
82
103
  with open(file, 'rb') as f: plot['data'] = base64.b64encode(f.read()).decode('ascii')
83
104
  os.remove(file)
105
+ elif extensions:
106
+ described = describe(figure, file[:-4] + '.figure.json', bbox_inches='tight')
107
+ if described and described.get('problem'): plot['figureProblem'] = str(described['problem'])[:500]
108
+ elif described: plot['figure'] = described
84
109
  emit(plot)
85
110
  plt.close('all')
86
111
 
112
+ # Figures the code saved to files in the project (savefig): each is described as savefig wrote it,
113
+ # and named with its file when the cell ends, once the file is as the cell left it. A file saved twice
114
+ # keeps its last figure. Nothing is kept for a session on a server, or for a file outside the project.
115
+ saving = {'cell': None, 'folder': None, 'files': {}, 'most': 0}
116
+ def saved_figure(figure, fname, kwargs):
117
+ import matplotlib
118
+ cell, folder, files = saving['cell'], saving['folder'], saving['files']
119
+ if not cell or not folder or not isinstance(fname, (str, os.PathLike)): return
120
+ full = os.path.realpath(os.path.join(os.getcwd(), os.fspath(fname)))
121
+ # savefig adds the format's extension to a name that has none.
122
+ fmt = kwargs.get('format') or matplotlib.rcParams['savefig.format']
123
+ if not os.path.isfile(full) and os.path.isfile(f'{full}.{fmt}'): full = f'{full}.{fmt}'
124
+ root = os.path.realpath(os.getcwd())
125
+ if not full.startswith(root + os.sep) or full.startswith(os.path.realpath(folder) + os.sep) or not os.path.isfile(full): return
126
+ if full not in files and len(files) >= saving['most']: return
127
+ pending = files[full][0] if full in files else os.path.join(folder, f'{cell}-p{len(files) + 1}.figure.json')
128
+ bbox = kwargs.get('bbox_inches', matplotlib.rcParams['savefig.bbox'])
129
+ options = {'bbox_inches': None if bbox == 'standard' else bbox, 'pad_inches': kwargs.get('pad_inches', matplotlib.rcParams['savefig.pad_inches'])}
130
+ files[full] = (pending, describe(figure, pending, **options))
131
+
132
+ def watch_savefig(module):
133
+ Figure = getattr(module, 'Figure', None)
134
+ if Figure is None or getattr(Figure.savefig, '_rho', False): return
135
+ original = Figure.savefig
136
+ @functools.wraps(original)
137
+ def savefig(self, fname, *args, **kwargs):
138
+ result = original(self, fname, *args, **kwargs)
139
+ try: saved_figure(self, fname, kwargs)
140
+ except Exception: pass
141
+ return result
142
+ savefig._rho = True
143
+ Figure.savefig = savefig
144
+
145
+ # matplotlib.figure is watched as it is first imported, whichever cell imports it.
146
+ class WatchFigures:
147
+ def find_spec(self, name, path=None, target=None):
148
+ if name != 'matplotlib.figure': return None
149
+ for finder in sys.meta_path:
150
+ if finder is self or not hasattr(finder, 'find_spec'): continue
151
+ spec = finder.find_spec(name, path, target)
152
+ if spec is None: continue
153
+ loader = spec.loader
154
+ if loader is not None and hasattr(loader, 'exec_module'):
155
+ run = loader.exec_module
156
+ def exec_module(module, run=run):
157
+ run(module)
158
+ try: watch_savefig(module)
159
+ except Exception: pass
160
+ loader.exec_module = exec_module
161
+ return spec
162
+ return None
163
+ if extensions:
164
+ if 'matplotlib.figure' in sys.modules: watch_savefig(sys.modules['matplotlib.figure'])
165
+ else: sys.meta_path.insert(0, WatchFigures())
166
+
167
+ def emit_saved(cell):
168
+ root, k = os.path.realpath(os.getcwd()), 0
169
+ for full, (pending, described) in saving['files'].items():
170
+ if not os.path.isfile(full): continue
171
+ k += 1
172
+ out = {'id': cell, 'kind': 'saved', 'path': os.path.relpath(full, root).replace(os.sep, '/'), 'number': k}
173
+ if described and described.get('problem'): out['figureProblem'] = str(described['problem'])[:500]
174
+ elif described and os.path.isfile(pending):
175
+ record = os.path.join(saving['folder'], f'{cell}-s{k}.figure.json')
176
+ os.replace(pending, record)
177
+ out['figure'] = dict(described, file=os.path.basename(record))
178
+ emit(out)
179
+ for pending, _ in saving['files'].values():
180
+ if os.path.isfile(pending):
181
+ try: os.remove(pending)
182
+ except OSError: pass
183
+
87
184
  # What a cell's code reads and assigns, from its parsed text before it runs: the names it loads,
88
185
  # the names it binds (also through df[...] = or obj.attr =, def, class and import), and its
89
186
  # strings. What a function, lambda or comprehension binds is its own, and so is a name the cell
@@ -210,6 +307,8 @@ def run(msg):
210
307
  name = f"<cell {msg.get('number', '')}>"
211
308
  linecache.cache[name] = (len(msg['code']), None, msg['code'].splitlines(True), name)
212
309
  sys.stdout, sys.stderr = Stream(cell, 'stdout'), Stream(cell, 'stderr')
310
+ if extensions and not msg.get('inline') and msg.get('plots'): saving.update(cell=cell, folder=msg['plots'], files={}, most=msg['maxPlots'])
311
+ cell_names[:] = list(assigned) + sorted(n for n in mentioned if n not in assigned)
213
312
  try:
214
313
  tree = ast.parse(msg['code'], name, 'exec')
215
314
  last = tree.body.pop() if tree.body and isinstance(tree.body[-1], ast.Expr) else None
@@ -227,8 +326,14 @@ def run(msg):
227
326
  emit({'id': cell, 'kind': 'error', 'text': 'Traceback (most recent call last):\n' + text if frames else text})
228
327
  finally:
229
328
  sys.stdout, sys.stderr = real_out, real_err
329
+ # The cell's own pictures are not files it saved.
330
+ saving['cell'] = None
230
331
  try: save_figures(cell, msg['plots'], msg['maxPlots'], msg.get('inline', False))
231
332
  except Exception as error: emit({'id': cell, 'kind': 'stderr', 'text': f'The figures could not be saved: {error}\n'})
333
+ if saving['files']:
334
+ try: emit_saved(cell)
335
+ except Exception: pass
336
+ saving.update(cell=None, folder=None, files={}, most=0)
232
337
  emit({'id': cell, 'kind': 'done', 'ok': ok, 'interrupted': interrupted, 'variables': variables(msg['maxVariables']),
233
338
  'used': sorted(mentioned & known), 'assigned': assigned, 'files': touched(files)})
234
339
 
@@ -13,6 +13,11 @@ Selected services: Only the root, bytes, resolve and register ports of Files; no
13
13
 
14
14
  Only this plugin depends on csv-parse. No scientific column assumptions or chart renderer.
15
15
 
16
+ A CSV opened as observed is read lexically, every value as text. A column its header leaves
17
+ unnamed is named: R's `write.csv()` and pandas' `to_csv()` write row names or the index under an
18
+ empty header, which becomes `row`; another unnamed column becomes `column N`, and a name the
19
+ header already uses is not taken twice.
20
+
16
21
  The package depends on `@rho-workspace/plugin-contracts` 0.4.0 for generic envelopes and service ports
17
22
  and ordinary file primitives; that package contains no capability implementation.
18
23
  Service ports do not install or start other plugins. Rho explicitly selects and
@@ -5,6 +5,7 @@ export declare const tableLimits: {
5
5
  maxRows: number;
6
6
  maxPageRows: number;
7
7
  };
8
+ export declare function csvColumnNames(header: string[]): string[];
8
9
  export declare function lexicalCsvDeclaration(data: Buffer, relative: string): Declaration;
9
10
  export declare function validateTableDeclaration(d: Declaration): void;
10
11
  export declare const declarationValidator: {
@@ -4,12 +4,28 @@ import { parse } from 'csv-parse/sync';
4
4
  import { atomicJson, canonical, confined, digest, publicResource, requireThat, stateDirectory, validateDeclarationEnvelope as validateEnvelope } from '@rho-workspace/plugin-contracts';
5
5
  const declaredSkill = (id, content, version = '1.0') => ({ id, version, content, digest: digest(content) });
6
6
  export const tableLimits = { maxRows: 100000, maxPageRows: 1000 };
7
+ // A column a CSV leaves unnamed is named: R's write.csv() and pandas' to_csv() write row names
8
+ // or the index under an empty header, which is "row"; another unnamed column is "column N". A name
9
+ // the header already uses is not taken twice. The row reader names the header the same way.
10
+ export function csvColumnNames(header) {
11
+ const taken = new Set(header.filter(Boolean));
12
+ return header.map((name, i) => {
13
+ if (name)
14
+ return name;
15
+ const base = i === 0 ? 'row' : `column ${i + 1}`;
16
+ let named = base;
17
+ for (let k = 2; taken.has(named); k++)
18
+ named = `${base} ${k}`;
19
+ taken.add(named);
20
+ return named;
21
+ });
22
+ }
7
23
  // Opening an observed CSV supplies a lexical view, with no guessed numeric,
8
24
  // missing-value or entity semantics. The usual table reader validates all rows.
9
25
  export function lexicalCsvDeclaration(data, relative) {
10
26
  requireThat(data.length <= 8 * 1024 * 1024, 'limit', 'CSV preview exceeds 8 MiB.');
11
27
  const records = parse(data, { bom: true, skip_empty_lines: true, max_record_size: 1024 * 1024, to: 1 });
12
- const declaration = { path: relative, kind: 'table', format: { encoding: 'csv', columns: (records[0] ?? []).map(name => ({ name, type: 'string', nullValues: [] })), key: null } };
28
+ const declaration = { path: relative, kind: 'table', format: { encoding: 'csv', columns: csvColumnNames(records[0] ?? []).map(name => ({ name, type: 'string', nullValues: [] })), key: null } };
13
29
  validateTableDeclaration(declaration);
14
30
  return declaration;
15
31
  }
@@ -50,7 +66,7 @@ export class Tables {
50
66
  else {
51
67
  const records = parse(data, { bom: true, skip_empty_lines: true, max_record_size: 1024 * 1024 });
52
68
  const header = records.shift() ?? [];
53
- requireThat(canonical(header) === canonical(f.columns.map(c => c.name)), 'condition', 'CSV headers do not match the declared columns.');
69
+ requireThat(canonical(csvColumnNames(header)) === canonical(f.columns.map(c => c.name)), 'condition', 'CSV headers do not match the declared columns.');
54
70
  requireThat(records.length <= this.limits.maxRows, 'limit', 'Table exceeds the row reader budget.', { limit: 'maxRows' });
55
71
  rows = records.map(values => Object.fromEntries(f.columns.map((c, i) => {
56
72
  const value = values[i];
@@ -160,6 +176,6 @@ const tablesSkill = {
160
176
  ],
161
177
  verify: { command: 'npm run test:tables', description: 'Declared types keep leading zeros and literal NA, joins retain nulls, namespaces and both input versions, and unkeyed tables never fabricate keys.' }
162
178
  };
163
- export const plugin = { entryVersion: '1.0', id: 'tables', package: '@rho-workspace/plugin-tables', version: '0.5.0', requires: ['files'], capabilities: ['tables'], presentation,
179
+ export const plugin = { entryVersion: '1.0', id: 'tables', package: '@rho-workspace/plugin-tables', version: '0.5.2', requires: ['files'], capabilities: ['tables'], presentation,
164
180
  operations: Object.entries(operationContracts).map(([method, contract]) => ({ method, presentation: operationPresentation[method], version: '2.0', ...structuredClone(contract), requires: [...contract.requires] })), skills: [{ ...tablesSkill, presentation: skillPresentation }],
165
181
  components: [{ id: 'rho.table', version: '0.2.0', owner: 'rho-plugins.table', bindings: { data: 'table' }, ui: './ui', presentation: viewPresentation, propsSchema: { type: 'object', additionalProperties: false, properties: { title: { type: 'string', minLength: 1 } }, required: ['title'] } }] };
@@ -230,7 +230,7 @@ function BoundTable({ view, projectId, client, selectable = true, active, render
230
230
  return _jsxs("div", { ref: root, style: { display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }, children: [_jsxs("div", { style: bar, role: "toolbar", "aria-label": "Table", children: [_jsx("span", { children: count }), selected > 0 && _jsxs("span", { children: ["\u00B7 ", selected, " selected"] }), selected > 0 && requestFeedback && _jsx("button", { style: control, onClick: () => { void selectionQueue.current.then(() => { if (selectionConfirmed.current)
231
231
  requestFeedback();
232
232
  else
233
- setFailure('Confirm the selected rows before asking Claude Code.'); }); }, children: "Ask Claude Code" }), filter && _jsxs("span", { style: { ...control, display: 'inline-flex', gap: 6, alignItems: 'center' }, children: [filter.column, " ", opLabel[filter.op], filter.op !== 'isNull' && ' ' + String(filter.value), _jsx("button", { "aria-label": "Remove filter", style: { border: 'none', background: 'none', cursor: 'pointer', padding: 0, color: 'inherit' }, onClick: () => applyFilter(null), children: "\u00D7" })] }), !filter && meta && !draft && _jsx("button", { style: control, onClick: () => setDraft({ column: meta.columns[0].name, op: 'eq', value: '' }), children: "Filter" }), draft && meta && _jsxs("form", { style: { display: 'flex', gap: 6, flexWrap: 'wrap', maxWidth: '100%' }, onSubmit: e => { e.preventDefault(); submitDraft(); }, children: [_jsx("select", { "aria-label": "Column", style: control, value: draft.column, onChange: e => setDraft({ ...draft, column: e.target.value, op: 'eq', value: '' }), children: meta.columns.map(c => _jsx("option", { children: c.name }, c.name)) }), _jsx("select", { "aria-label": "Condition", style: control, value: draft.op, onChange: e => setDraft({ ...draft, op: e.target.value }), children: Object.keys(opLabel).filter(op => !(['lt', 'gt'].includes(op) && meta.columns.find(c => c.name === draft.column)?.type === 'boolean')).map(op => _jsx("option", { value: op, children: opLabel[op] }, op)) }), draft.op !== 'isNull' && _jsx("input", { "aria-label": "Value", style: control, value: draft.value, onChange: e => setDraft({ ...draft, value: e.target.value }) }), _jsx("button", { style: control, type: "submit", children: "Apply" }), _jsx("button", { style: control, type: "button", onClick: () => setDraft(null), children: "Cancel" })] }), meta && !meta.entityKey && selectable && _jsx("span", { style: { color: 'var(--text-muted, #5f6873)' }, children: "Rows have no key, so they can't be selected for Claude Code." })] }), failure && _jsxs("div", { role: "alert", style: { ...bar, color: 'var(--danger, #cf222e)' }, children: [failure, _jsx("button", { style: control, onClick: () => { setAttempt(n => n + 1); api.current?.purgeInfiniteCache(); }, children: "Try again" })] }), observationFailure && _jsx("div", { role: "status", style: bar, children: observationFailure }), _jsx("div", { style: { flex: 1, minHeight: 0, maskImage: more ? 'linear-gradient(to right, #000 calc(100% - 48px), transparent)' : undefined }, children: meta && _jsx(AgGridReact, { theme: theme, rowModelType: "infinite", datasource: datasource, cacheBlockSize: pageRows, maxBlocksInCache: 5, suppressMultiSort: true, maxConcurrentDatasourceRequests: 1, columnDefs: columnDefs,
233
+ setFailure('Confirm the selected rows before asking the Agent.'); }); }, children: "Ask Agent" }), filter && _jsxs("span", { style: { ...control, display: 'inline-flex', gap: 6, alignItems: 'center' }, children: [filter.column, " ", opLabel[filter.op], filter.op !== 'isNull' && ' ' + String(filter.value), _jsx("button", { "aria-label": "Remove filter", style: { border: 'none', background: 'none', cursor: 'pointer', padding: 0, color: 'inherit' }, onClick: () => applyFilter(null), children: "\u00D7" })] }), !filter && meta && !draft && _jsx("button", { style: control, onClick: () => setDraft({ column: meta.columns[0].name, op: 'eq', value: '' }), children: "Filter" }), draft && meta && _jsxs("form", { style: { display: 'flex', gap: 6, flexWrap: 'wrap', maxWidth: '100%' }, onSubmit: e => { e.preventDefault(); submitDraft(); }, children: [_jsx("select", { "aria-label": "Column", style: control, value: draft.column, onChange: e => setDraft({ ...draft, column: e.target.value, op: 'eq', value: '' }), children: meta.columns.map(c => _jsx("option", { children: c.name }, c.name)) }), _jsx("select", { "aria-label": "Condition", style: control, value: draft.op, onChange: e => setDraft({ ...draft, op: e.target.value }), children: Object.keys(opLabel).filter(op => !(['lt', 'gt'].includes(op) && meta.columns.find(c => c.name === draft.column)?.type === 'boolean')).map(op => _jsx("option", { value: op, children: opLabel[op] }, op)) }), draft.op !== 'isNull' && _jsx("input", { "aria-label": "Value", style: control, value: draft.value, onChange: e => setDraft({ ...draft, value: e.target.value }) }), _jsx("button", { style: control, type: "submit", children: "Apply" }), _jsx("button", { style: control, type: "button", onClick: () => setDraft(null), children: "Cancel" })] }), meta && !meta.entityKey && selectable && _jsx("span", { style: { color: 'var(--text-muted, #5f6873)' }, children: "This table has no row key; row selection is unavailable." })] }), failure && _jsxs("div", { role: "alert", style: { ...bar, color: 'var(--danger, #cf222e)' }, children: [failure, _jsx("button", { style: control, onClick: () => { setAttempt(n => n + 1); api.current?.purgeInfiniteCache(); }, children: "Try again" })] }), observationFailure && _jsx("div", { role: "status", style: bar, children: observationFailure }), _jsx("div", { style: { flex: 1, minHeight: 0, maskImage: more ? 'linear-gradient(to right, #000 calc(100% - 48px), transparent)' : undefined }, children: meta && _jsx(AgGridReact, { theme: theme, rowModelType: "infinite", datasource: datasource, cacheBlockSize: pageRows, maxBlocksInCache: 5, suppressMultiSort: true, maxConcurrentDatasourceRequests: 1, columnDefs: columnDefs,
234
234
  // Columns take their contents' width, headers included, so a number is never cut to
235
235
  // an ellipsis that hides its exponent; a wide table scrolls sideways instead.
236
236
  defaultColDef: { resizable: true, minWidth: 64 }, suppressColumnVirtualisation: (meta?.columns.length ?? 0) <= 40, onFirstDataRendered: fitColumns, getRowId: meta?.entityKey && meta.namespace ? p => meta.namespace + ':' + String(p.data[meta.entityKey]) : undefined, rowSelection: meta?.entityKey && meta.namespace && selectable ? { mode: 'multiRow', checkboxes: true, headerCheckbox: false, enableClickSelection: true } : undefined, onModelUpdated: e => { if (meta.entityKey)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rho-workspace/plugin-tables",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "description": "Rho tables plugin",
6
6
  "license": "AGPL-3.0-only",
@@ -199,7 +199,7 @@ var EventV2Processor = class extends EventProcessor {
199
199
  const headers = new Headers();
200
200
  this.getCookies(event, headers);
201
201
  if (event.headers) {
202
- for (const [k, v] of Object.entries(event.headers)) if (v) headers.set(k, v);
202
+ for (const [k, v] of Object.entries(event.headers)) if (v) headers.set(k, sanitizeHeaderValue(v));
203
203
  }
204
204
  return headers;
205
205
  }