@rho-workspace/rho 0.5.3 → 0.5.4

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 (216) hide show
  1. package/README.md +24 -5
  2. package/dist/capabilities.d.ts +27 -0
  3. package/dist/capabilities.js +73 -1
  4. package/dist/cli.js +150 -25
  5. package/dist/contract.generated.d.ts +112 -12
  6. package/dist/contracts/owners.generated.js +103 -8
  7. package/dist/conversation-presentation.d.ts +3 -1
  8. package/dist/conversation-presentation.js +16 -6
  9. package/dist/conversations.d.ts +10 -6
  10. package/dist/conversations.js +17 -13
  11. package/dist/installs.d.ts +11 -0
  12. package/dist/installs.js +179 -0
  13. package/dist/project-notes.js +2 -0
  14. package/dist/protocol.js +7 -5
  15. package/dist/screen-check-skill.d.ts +2 -0
  16. package/dist/screen-check-skill.js +39 -0
  17. package/dist/server.d.ts +2 -0
  18. package/dist/server.js +50 -7
  19. package/dist/web/.vite/manifest.json +51 -51
  20. package/dist/web/assets/{choices-B-y2OYvE.js → choices-DU9kHlyQ.js} +1 -1
  21. package/dist/web/assets/{console-e1P5qYQ5.js → console-Dbdo-5oO.js} +1 -1
  22. package/dist/web/assets/{dist-Dd3iAGFy.js → dist-B2V0VDO0.js} +1 -1
  23. package/dist/web/assets/{dist-CH56yNDn.js → dist-BV3hWfn_.js} +1 -1
  24. package/dist/web/assets/{dist-CBM2oq6g.js → dist-CLNF2g-E.js} +1 -1
  25. package/dist/web/assets/{dist-DEorbOW0.js → dist-D25Vn17i.js} +1 -1
  26. package/dist/web/assets/{explore-in-muWXOGOY.js → explore-in-8Q5ftt-E.js} +1 -1
  27. package/dist/web/assets/{graph-CK-df5lh.js → graph-hS2kvHTI.js} +1 -1
  28. package/dist/web/assets/{index-BYuKpyoi.js → index-BHQIaDsx.js} +220 -121
  29. package/dist/web/assets/index-Cj1_iqyK.css +1 -0
  30. package/dist/web/assets/{notes-meaning-B3HFbh84.js → notes-meaning-DdPbtNzJ.js} +1 -1
  31. package/dist/web/assets/{overview-D1Ev2OC_.js → overview-BV6ZO5TO.js} +3 -3
  32. package/dist/web/assets/{preview-HbmQKUGY.js → preview-n5lOOdjN.js} +1 -1
  33. package/dist/web/assets/{review-BVh9jWMB.js → review-CmWou4Iv.js} +1 -1
  34. package/dist/web/assets/{skills-DPaEYkln.js → skills-Dy7n3L9d.js} +1 -1
  35. package/dist/web/assets/{skills-work-dvjfy_tu.js → skills-work-Cj0W6b-I.js} +1 -1
  36. package/dist/web/assets/{studio-process-BYm8mjRJ.js → studio-process-QOY_n2_8.js} +1 -1
  37. package/dist/web/assets/{timeline-events-CDZ7wiJX.js → timeline-events-DmX1BCh3.js} +1 -1
  38. package/dist/web/assets/{work-turns-BeCt303c.js → work-turns-Co0C71rJ.js} +1 -1
  39. package/dist/web/assets/{workspace-d5Hw_ClZ.js → workspace-BKFefRfJ.js} +2 -2
  40. package/dist/web/composition.json +73 -73
  41. package/dist/web/index.html +2 -2
  42. package/dist/web/renderer.json +1 -1
  43. package/docs/FRONTEND.md +30 -14
  44. package/docs/HEADLESS.md +59 -13
  45. package/example/pa14-qs/.rho/rho/conversations/c-67f77cfebaba2cea.json +1 -1
  46. package/example/pa14-qs/.rho/rho/conversations/c-91aa2e46b1ac1a6c.json +1 -1
  47. package/example/pa14-qs/.rho/rho/conversations/c-fa24dfd80dfe4bcb.json +1 -1
  48. package/node_modules/@hono/node-server/dist/index.js +11 -0
  49. package/node_modules/@hono/node-server/dist/index.mjs +11 -0
  50. package/node_modules/@hono/node-server/dist/listener.js +11 -0
  51. package/node_modules/@hono/node-server/dist/listener.mjs +11 -0
  52. package/node_modules/@hono/node-server/dist/request.js +11 -0
  53. package/node_modules/@hono/node-server/dist/request.mjs +11 -0
  54. package/node_modules/@hono/node-server/dist/server.js +11 -0
  55. package/node_modules/@hono/node-server/dist/server.mjs +11 -0
  56. package/node_modules/@hono/node-server/dist/vercel.js +11 -0
  57. package/node_modules/@hono/node-server/dist/vercel.mjs +11 -0
  58. package/node_modules/@hono/node-server/package.json +1 -1
  59. package/node_modules/@rho-workspace/plugin-agent/README.md +3 -1
  60. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +14 -3
  61. package/node_modules/@rho-workspace/plugin-agent/dist/conversation.d.ts +1 -0
  62. package/node_modules/@rho-workspace/plugin-agent/dist/conversation.js +1 -0
  63. package/node_modules/@rho-workspace/plugin-agent/dist/generic-acp.d.ts +11 -0
  64. package/node_modules/@rho-workspace/plugin-agent/dist/generic-acp.js +7 -0
  65. package/node_modules/@rho-workspace/plugin-agent/dist/mcp.js +5 -1
  66. package/node_modules/@rho-workspace/plugin-agent/package.json +1 -1
  67. package/node_modules/@rho-workspace/plugin-annotations/package.json +1 -1
  68. package/node_modules/@rho-workspace/plugin-documents/README.md +4 -0
  69. package/node_modules/@rho-workspace/plugin-documents/dist/index.d.ts +3 -0
  70. package/node_modules/@rho-workspace/plugin-documents/dist/index.js +1 -1
  71. package/node_modules/@rho-workspace/plugin-documents/package.json +1 -1
  72. package/node_modules/@rho-workspace/plugin-kernel/README.md +71 -10
  73. package/node_modules/@rho-workspace/plugin-kernel/dist/code.d.ts +2 -0
  74. package/node_modules/@rho-workspace/plugin-kernel/dist/code.js +4 -1
  75. package/node_modules/@rho-workspace/plugin-kernel/dist/contract.js +5 -0
  76. package/node_modules/@rho-workspace/plugin-kernel/dist/index.d.ts +7 -0
  77. package/node_modules/@rho-workspace/plugin-kernel/dist/index.js +55 -8
  78. package/node_modules/@rho-workspace/plugin-kernel/dist/lineage.d.ts +9 -6
  79. package/node_modules/@rho-workspace/plugin-kernel/dist/lineage.js +18 -41
  80. package/node_modules/@rho-workspace/plugin-kernel/dist/operations.js +5 -2
  81. package/node_modules/@rho-workspace/plugin-kernel/dist/script-flow.d.ts +39 -0
  82. package/node_modules/@rho-workspace/plugin-kernel/dist/script-flow.js +661 -0
  83. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-base.d.ts +3 -0
  84. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-base.js +10 -0
  85. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-flow.d.ts +38 -4
  86. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-flow.js +194 -97
  87. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook-work.js +1 -2
  88. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.d.ts +10 -2
  89. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.js +79 -18
  90. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-styles.js +115 -45
  91. package/node_modules/@rho-workspace/plugin-kernel/dist/ui.d.ts +1 -1
  92. package/node_modules/@rho-workspace/plugin-kernel/dist/ui.js +1 -1
  93. package/node_modules/@rho-workspace/plugin-kernel/dist/value-graph.d.ts +63 -0
  94. package/node_modules/@rho-workspace/plugin-kernel/dist/value-graph.js +344 -0
  95. package/node_modules/@rho-workspace/plugin-kernel/package.json +1 -1
  96. package/node_modules/@rho-workspace/plugin-kernel/runners/runner.R +6 -3
  97. package/node_modules/@rho-workspace/plugin-kernel/runners/runner.py +6 -3
  98. package/node_modules/express-rate-limit/dist/index.cjs +1 -1
  99. package/node_modules/express-rate-limit/dist/index.mjs +1 -1
  100. package/node_modules/express-rate-limit/package.json +1 -1
  101. package/node_modules/ip-address/dist/ipv6.js +1 -1
  102. package/node_modules/ip-address/package.json +1 -1
  103. package/node_modules/playwright-core/LICENSE +202 -0
  104. package/node_modules/playwright-core/NOTICE +5 -0
  105. package/node_modules/playwright-core/ThirdPartyNotices.txt +13 -0
  106. package/node_modules/playwright-core/bin/install_media_pack.ps1 +5 -0
  107. package/node_modules/playwright-core/bin/install_webkit_wsl.ps1 +33 -0
  108. package/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh +42 -0
  109. package/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh +13 -0
  110. package/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 +24 -0
  111. package/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh +42 -0
  112. package/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh +12 -0
  113. package/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 +24 -0
  114. package/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh +48 -0
  115. package/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh +11 -0
  116. package/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 +23 -0
  117. package/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh +48 -0
  118. package/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh +11 -0
  119. package/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 +23 -0
  120. package/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh +48 -0
  121. package/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh +11 -0
  122. package/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 +24 -0
  123. package/node_modules/playwright-core/browsers.json +52 -0
  124. package/node_modules/playwright-core/cli.js +22 -0
  125. package/node_modules/playwright-core/index.js +17 -0
  126. package/node_modules/playwright-core/index.mjs +28 -0
  127. package/node_modules/playwright-core/lib/bootstrap.js +149 -0
  128. package/node_modules/playwright-core/lib/coreBundle.js +78041 -0
  129. package/node_modules/playwright-core/lib/entry/cliDaemon.js +5 -0
  130. package/node_modules/playwright-core/lib/entry/dashboardApp.js +3 -0
  131. package/node_modules/playwright-core/lib/entry/mcp.js +10 -0
  132. package/node_modules/playwright-core/lib/entry/oopBrowserDownload.js +3 -0
  133. package/node_modules/playwright-core/lib/package.js +50 -0
  134. package/node_modules/playwright-core/lib/server/chromium/appIcon.png +0 -0
  135. package/node_modules/playwright-core/lib/server/electron/loader.js +123 -0
  136. package/node_modules/playwright-core/lib/serverRegistry.js +1939 -0
  137. package/node_modules/playwright-core/lib/serverRegistry.js.LICENSE +65 -0
  138. package/node_modules/playwright-core/lib/tools/cli-client/channelSessions.js +141 -0
  139. package/node_modules/playwright-core/lib/tools/cli-client/cli.js +7 -0
  140. package/node_modules/playwright-core/lib/tools/cli-client/help.json +808 -0
  141. package/node_modules/playwright-core/lib/tools/cli-client/minimist.js +128 -0
  142. package/node_modules/playwright-core/lib/tools/cli-client/output.js +352 -0
  143. package/node_modules/playwright-core/lib/tools/cli-client/program.js +422 -0
  144. package/node_modules/playwright-core/lib/tools/cli-client/registry.js +180 -0
  145. package/node_modules/playwright-core/lib/tools/cli-client/session.js +261 -0
  146. package/node_modules/playwright-core/lib/tools/dashboard/appIcon.png +0 -0
  147. package/node_modules/playwright-core/lib/tools/utils/extension.js +112 -0
  148. package/node_modules/playwright-core/lib/tools/utils/socketConnection.js +108 -0
  149. package/node_modules/playwright-core/lib/utilsBundle.js +86078 -0
  150. package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +1890 -0
  151. package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule--QdMvsKi.css +1 -0
  152. package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule-CQ8RZGBm.js +32 -0
  153. package/node_modules/playwright-core/lib/vite/dashboard/assets/codicon-DCmgc-ay.ttf +0 -0
  154. package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-1bWoP6pv.svg +1 -0
  155. package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-beta-k3eOH_eK.svg +1 -0
  156. package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-nightly-Cp5nfeDT.svg +1 -0
  157. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-AwgwcYie.css +1 -0
  158. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-hRvO4yU2.js +12 -0
  159. package/node_modules/playwright-core/lib/vite/dashboard/assets/safari-na3_-uQk.svg +1 -0
  160. package/node_modules/playwright-core/lib/vite/dashboard/index.html +29 -0
  161. package/node_modules/playwright-core/lib/vite/dashboard/playwright-logo.svg +24 -0
  162. package/node_modules/playwright-core/lib/vite/htmlReport/index.html +16 -0
  163. package/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -0
  164. package/node_modules/playwright-core/lib/vite/htmlReport/report.js +32 -0
  165. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
  166. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DK72OMRi.js +32 -0
  167. package/node_modules/playwright-core/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf +0 -0
  168. package/node_modules/playwright-core/lib/vite/recorder/assets/index-BZpYJZ-P.css +1 -0
  169. package/node_modules/playwright-core/lib/vite/recorder/assets/index-Cn0mmDuv.js +129 -0
  170. package/node_modules/playwright-core/lib/vite/recorder/index.html +29 -0
  171. package/node_modules/playwright-core/lib/vite/recorder/playwright-logo.svg +9 -0
  172. package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-BEB2e8M3.js +32 -0
  173. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-D-g2vrvf.js +184 -0
  174. package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
  175. package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-B794rD2Z.js +7 -0
  176. package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
  177. package/node_modules/playwright-core/lib/vite/traceViewer/codicon.DCmgc-ay.ttf +0 -0
  178. package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.C8BeHuxd.css +1 -0
  179. package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
  180. package/node_modules/playwright-core/lib/vite/traceViewer/index.html +44 -0
  181. package/node_modules/playwright-core/lib/vite/traceViewer/index.oNry3Hjv.js +1 -0
  182. package/node_modules/playwright-core/lib/vite/traceViewer/manifest.webmanifest +16 -0
  183. package/node_modules/playwright-core/lib/vite/traceViewer/playwright-logo.svg +9 -0
  184. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
  185. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +10 -0
  186. package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +4 -0
  187. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CYwC_gH1.js +5 -0
  188. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CyMfwkXJ.css +1 -0
  189. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +18 -0
  190. package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
  191. package/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
  192. package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
  193. package/node_modules/playwright-core/lib/xdg-open +1267 -0
  194. package/node_modules/playwright-core/package.json +34 -0
  195. package/package.json +10 -6
  196. package/protocol/index.json +17 -17
  197. package/protocol/operations/conversation.settings.check.json +147 -4
  198. package/protocol/operations/conversation.settings.read.json +145 -3
  199. package/protocol/operations/conversation.settings.update.json +166 -4
  200. package/protocol/operations/conversation.start.json +2 -1
  201. package/protocol/operations/kernel.interrupt.json +20 -0
  202. package/protocol/operations/kernel.read.json +30 -0
  203. package/protocol/operations/kernel.run.json +36 -1
  204. package/protocol/operations/kernel.stop.json +10 -0
  205. package/scripts/browser-checks.mjs +53 -0
  206. package/scripts/screen-check.mjs +193 -0
  207. package/vendor/composition.json +102 -102
  208. package/vendor/rho-workspace-plugin-agent-0.4.28.tgz +0 -0
  209. package/vendor/rho-workspace-plugin-annotations-0.5.2.tgz +0 -0
  210. package/vendor/rho-workspace-plugin-documents-0.4.11.tgz +0 -0
  211. package/vendor/rho-workspace-plugin-kernel-0.5.5.tgz +0 -0
  212. package/web/theme.css +17 -2
  213. package/dist/web/assets/index-DBd77u4a.css +0 -1
  214. package/vendor/rho-workspace-plugin-agent-0.4.27.tgz +0 -0
  215. package/vendor/rho-workspace-plugin-documents-0.4.10.tgz +0 -0
  216. package/vendor/rho-workspace-plugin-kernel-0.5.2.tgz +0 -0
@@ -1,7 +1,7 @@
1
1
  import { projectPath, commandFiles, buildWorkGraph } from './work-graph.js';
2
2
  import { notesInstructions, projectNotesPath } from './project-notes.js';
3
3
  import { addSpend, describeLimits, describable, fileFacts, figureContext, raster } from './file-describer.js';
4
- import { agentTurnLimit, toolActivity } from './conversation-presentation.js';
4
+ import { agentTurnLimit, displayCommand, toolActivity } from './conversation-presentation.js';
5
5
  import { privateEvidencePath } from './evidence-capture.js';
6
6
  import * as fs from 'node:fs/promises';
7
7
  import path from 'node:path';
@@ -127,7 +127,7 @@ function compare(before, after, limit) {
127
127
  changed.sort((a, b) => (a.modifiedAt ?? '\uffff').localeCompare(b.modifiedAt ?? '\uffff') || a.path.localeCompare(b.path));
128
128
  return { changed: changed.slice(0, limit), omitted: Math.max(0, changed.length - limit), partial };
129
129
  }
130
- const backends = ['claude-acp', 'codex-acp'];
130
+ const backends = ['claude-acp', 'codex-acp', 'acp'];
131
131
  const blankSettings = () => ({ format: 1, version: 0, updatedAt: null, backend: null, choices: {}, offered: {} });
132
132
  const acpDriver = (driver) => !!driver && 'configure' in driver;
133
133
  const identity = /^c-[a-f0-9]{16}$/;
@@ -196,7 +196,7 @@ export class Conversations {
196
196
  this.limits = { ...conversationLimits, ...limits };
197
197
  for (const key of ['maxCaptureFiles', 'maxCaptureBytes', 'maxSnapshotBytes', 'maxCaptureConcurrent'])
198
198
  requireThat(Number.isSafeInteger(this.limits[key]) && this.limits[key] > 0 && this.limits[key] <= conversationLimits[key], 'invalid', 'Capture budgets must be positive integers no greater than the supported defaults.', { limit: key, maximum: conversationLimits[key] });
199
- requireThat(!config.backend || ['claude-acp', 'codex-acp', 'claude-cli'].includes(config.backend), 'invalid', 'Choose claude-acp, codex-acp or claude-cli.');
199
+ requireThat(!config.backend || ['claude-acp', 'codex-acp', 'claude-cli', 'acp'].includes(config.backend), 'invalid', 'Choose claude-acp, codex-acp, claude-cli or acp.');
200
200
  }
201
201
  serial(key, run) {
202
202
  const task = (this.queues.get(key) ?? Promise.resolve()).catch(() => { }).then(run);
@@ -408,7 +408,7 @@ export class Conversations {
408
408
  const saved = await this.readSettings();
409
409
  const backend = input.backend ?? saved.backend ?? this.config.backend ?? 'claude-acp';
410
410
  const config = backend === 'claude-cli' ? undefined : input.config ?? saved.choices[backend];
411
- const at = now(), c = { format: 2, backend, id: 'c-' + randomBytes(8).toString('hex'), changeId: input.changeId, requestDigest, createdAt: at, createdBy: by, updatedAt: at, version: 0, client: backend === 'codex-acp' ? 'codex' : 'claude-code', nativeSessionId: backend === 'claude-cli' ? randomUUID() : null, confirmed: false, principal: null, title: titleOf(text), turns: [], ...(config && Object.keys(config).length ? { config } : {}) };
411
+ const at = now(), c = { format: 2, backend, id: 'c-' + randomBytes(8).toString('hex'), changeId: input.changeId, requestDigest, createdAt: at, createdBy: by, updatedAt: at, version: 0, client: backend === 'codex-acp' ? 'codex' : backend === 'acp' ? 'acp' : 'claude-code', nativeSessionId: backend === 'claude-cli' ? randomUUID() : null, confirmed: false, principal: null, title: titleOf(text), turns: [], ...(config && Object.keys(config).length ? { config } : {}) };
412
412
  for (const id of input.contexts ?? [])
413
413
  await this.ports.context?.(project, id);
414
414
  const turn = this.turn(input.changeId, requestDigest, by, text, 1);
@@ -468,7 +468,7 @@ export class Conversations {
468
468
  this.choices({ [input.configId]: input.value });
469
469
  return this.serial(project.id, async () => {
470
470
  const c = await this.load(project, input.conversationId), last = c.turns.at(-1);
471
- requireThat(c.backend === 'claude-acp' || c.backend === 'codex-acp', 'unavailable', 'This conversation\'s Agent has no settings Rho can change.');
471
+ requireThat(c.backend === 'claude-acp' || c.backend === 'codex-acp' || c.backend === 'acp', 'unavailable', 'This conversation\'s Agent has no settings Rho can change.');
472
472
  requireThat(!c.trashedAt, 'conflict', 'This conversation is in the trash. Restore it to change its settings.');
473
473
  requireThat(!last || last.state !== 'running' || !this.liveTurn(project, c, last), 'conflict', 'The Agent is answering. Change its settings after this message.');
474
474
  const driver = this.drivers.get(project.id + '/' + c.id)?.driver;
@@ -558,9 +558,9 @@ export class Conversations {
558
558
  return this.settings();
559
559
  }
560
560
  async inspect(project, backend) {
561
- const { ClaudeAcpConversation, CodexAcpConversation } = await import('@rho-workspace/plugin-agent/conversation');
561
+ const { ClaudeAcpConversation, CodexAcpConversation, GenericAcpConversation } = await import('@rho-workspace/plugin-agent/conversation');
562
562
  const common = { cwd: project.root, mcpServers: [], limits: this.limits };
563
- const driver = backend === 'codex-acp' ? new CodexAcpConversation({ ...this.config.codexAcp, ...common, codex: this.config.codex }) : new ClaudeAcpConversation({ preferred: claudePreferred, ...this.config.claudeAcp, ...common, claude: this.binary });
563
+ const driver = backend === 'codex-acp' ? new CodexAcpConversation({ ...this.config.codexAcp, ...common, codex: this.config.codex }) : backend === 'acp' ? new GenericAcpConversation({ ...common, command: this.config.acpCommand }) : new ClaudeAcpConversation({ preferred: claudePreferred, ...this.config.claudeAcp, ...common, claude: this.binary });
564
564
  let found;
565
565
  try {
566
566
  found = await driver.inspect();
@@ -750,11 +750,11 @@ export class Conversations {
750
750
  const grant = await this.ports.grant(project, c);
751
751
  c.principal = grant.principal;
752
752
  const mcpServers = [{ name: 'rho', command: process.execPath, args: [fileURLToPath(import.meta.resolve('@rho-workspace/plugin-agent/mcp'))], env: [{ name: 'RHO_AGENT_CONNECTION', value: grant.file }] }];
753
- const { ClaudeAcpConversation, CodexAcpConversation, ClaudeCliConversation } = await import('@rho-workspace/plugin-agent/conversation');
753
+ const { ClaudeAcpConversation, CodexAcpConversation, ClaudeCliConversation, GenericAcpConversation } = await import('@rho-workspace/plugin-agent/conversation');
754
754
  const observed = { observation: c.observation, observe: (observation) => {
755
755
  requireThat(observation.sessionId === c.acpSessionId, 'conflict', 'The observed Agent session differs from the retained identity.');
756
756
  requireThat(Buffer.byteLength(JSON.stringify(observation)) <= this.limits.maxObservationBytes, 'limit', 'The session projection exceeded maxObservationBytes; prior observations were kept.');
757
- const config = observation.config, learned = config && config.at !== c.observation?.config?.at && config.origin === 'initial' && !c.config && (c.backend === 'claude-acp' || c.backend === 'codex-acp');
757
+ const config = observation.config, learned = config && config.at !== c.observation?.config?.at && config.origin === 'initial' && !c.config && (c.backend === 'claude-acp' || c.backend === 'codex-acp' || c.backend === 'acp');
758
758
  c.observation = observation;
759
759
  this.schedule(project, c);
760
760
  for (const live of this.live.values())
@@ -775,7 +775,9 @@ export class Conversations {
775
775
  ? new CodexAcpConversation({ ...this.config.codexAcp, ...common, codex: this.config.codex })
776
776
  : c.backend === 'claude-acp'
777
777
  ? new ClaudeAcpConversation({ preferred: claudePreferred, ...this.config.claudeAcp, ...common, claude: this.binary, plugins: [...this.config.claudeAcp?.plugins ?? [], ...await this.ports.claudePlugins?.(project) ?? []] })
778
- : new ClaudeCliConversation({ cwd: project.root, binary: this.binary, sessionId: c.nativeSessionId, confirmed: c.confirmed, projectName: project.name ?? path.basename(project.root), mcpServers, limits: this.limits, instructions });
778
+ : c.backend === 'acp'
779
+ ? new GenericAcpConversation({ ...common, command: this.config.acpCommand })
780
+ : new ClaudeCliConversation({ cwd: project.root, binary: this.binary, sessionId: c.nativeSessionId, confirmed: c.confirmed, projectName: project.name ?? path.basename(project.root), mcpServers, limits: this.limits, instructions });
779
781
  this.drivers.set(key, { driver, project, conversation: c });
780
782
  return driver;
781
783
  });
@@ -895,8 +897,9 @@ export class Conversations {
895
897
  const action = toolActivity(incoming, null, project.root);
896
898
  if (action.targetKind === 'path' && (action.location || incoming.detail))
897
899
  this.capture(live, action.location ?? incoming.detail, 'step:' + incoming.id + ':' + incoming.outcome);
898
- if (action.targetKind === 'command' && action.target)
899
- for (const file of commandFiles(action.target, project))
900
+ // A command's files are captured however its row reads (an install names its packages).
901
+ if (action.action === 'execute' && incoming.detail)
902
+ for (const file of commandFiles(displayCommand(incoming.detail), project))
900
903
  this.capture(live, file, 'command:' + incoming.id + ':' + incoming.outcome);
901
904
  }
902
905
  if (!kept)
@@ -1068,7 +1071,8 @@ export class Conversations {
1068
1071
  const given = shown.get(file);
1069
1072
  const reason = !enabled ? 'Descriptions by a model are switched off; the facts are kept.'
1070
1073
  : c.backend === 'codex-acp' ? 'Descriptions are written with Claude Code; this session\'s Agent is Codex.'
1071
- : !this.ports.describe ? 'No model is set up to describe files.' : null;
1074
+ : c.backend === 'acp' ? 'Descriptions are written with Claude Code; this session\'s Agent is a configured ACP agent.'
1075
+ : !this.ports.describe ? 'No model is set up to describe files.' : null;
1072
1076
  const keep = (why) => { if (record.reason !== why) {
1073
1077
  record.state = record.sentence ? 'described' : 'facts';
1074
1078
  record.reason = why;
@@ -0,0 +1,11 @@
1
+ export interface Installed {
2
+ tool: string;
3
+ packages: Array<{
4
+ name: string;
5
+ version: string | null;
6
+ }>;
7
+ more: number;
8
+ from: string | null;
9
+ }
10
+ export declare function readInstall(command: string, printed?: string | null): Installed | null;
11
+ export declare function installText(installed: Installed): string;
@@ -0,0 +1,179 @@
1
+ // One command line split into its commands and words, honouring quotes. Pipes, `&&`, `||`, `;`,
2
+ // new lines and a subshell's parentheses separate commands.
3
+ function commands(line) {
4
+ const out = [];
5
+ let words = [], word = '', quote = '', has = false;
6
+ const end = () => { if (has)
7
+ words.push(word); word = ''; has = false; };
8
+ const next = () => { end(); if (words.length)
9
+ out.push(words); words = []; };
10
+ for (let i = 0; i < line.length; i++) {
11
+ const c = line[i];
12
+ if (quote) {
13
+ if (c === quote)
14
+ quote = '';
15
+ else if (c === '\\' && quote === '"' && i + 1 < line.length)
16
+ word += line[++i];
17
+ else
18
+ word += c;
19
+ continue;
20
+ }
21
+ if (c === '"' || c === "'") {
22
+ quote = c;
23
+ has = true;
24
+ continue;
25
+ }
26
+ // A backslash before a new line continues the command; before anything else it escapes it.
27
+ if (c === '\\' && line[i + 1] === '\n') {
28
+ end();
29
+ i++;
30
+ continue;
31
+ }
32
+ if (c === '\\' && i + 1 < line.length) {
33
+ word += line[++i];
34
+ has = true;
35
+ continue;
36
+ }
37
+ if (c === '\n' || c === ';' || c === '|' || c === '&' || c === '(' || c === ')') {
38
+ next();
39
+ if ((c === '|' || c === '&') && line[i + 1] === c)
40
+ i++;
41
+ continue;
42
+ }
43
+ if (c === ' ' || c === '\t') {
44
+ end();
45
+ continue;
46
+ }
47
+ word += c;
48
+ has = true;
49
+ }
50
+ next();
51
+ return out;
52
+ }
53
+ const base = (word) => word.split(/[\\/]/).pop().replace(/\.exe$/i, '');
54
+ // Options of these tools that take the next word as their value, so it is not a package.
55
+ const valued = new Set(['-e', '--editable', '-r', '--requirement', '-c', '--constraint', '--channel', '-i', '--index-url', '--extra-index-url', '-f', '--find-links', '-t', '--target', '--prefix', '--root', '-p', '--python', '-n', '--name', '--group', '--optional', '--extra', '--index', '--default-index', '--cache-dir', '--directory', '--project', '--package', '--override', '--build-constraint']);
56
+ // A package as an installer names it (pandas, pandas==2.2.3, pandas[excel]>=2, conda-forge::pandas);
57
+ // a path, a URL or a file is not one.
58
+ const spec = (raw) => {
59
+ const word = raw.includes('::') ? raw.slice(raw.lastIndexOf('::') + 2) : raw;
60
+ if (/[\\/]|^\.|:\/\/|\.(txt|toml|in|cfg|whl|gz|zip)$/i.test(word))
61
+ return null;
62
+ const name = word.match(/^[A-Za-z0-9][A-Za-z0-9._-]*/)?.[0];
63
+ if (!name)
64
+ return null;
65
+ return { name, version: word.slice(name.length).match(/^(?:\[[^\]]*\])?={1,2}([^,;\s=<>!~*]+)$/)?.[1] ?? null };
66
+ };
67
+ // A Python installer's words after `install` (or uv's `add`): its packages and a requirements file.
68
+ function pythonArgs(words) {
69
+ const packages = [];
70
+ let from = null;
71
+ for (let i = 0; i < words.length; i++) {
72
+ const word = words[i];
73
+ if (word === '-r' || word === '--requirement') {
74
+ from = words[i + 1] ?? null;
75
+ i++;
76
+ continue;
77
+ }
78
+ if (word.startsWith('--requirement=')) {
79
+ from = word.slice(14);
80
+ continue;
81
+ }
82
+ if (valued.has(word)) {
83
+ i++;
84
+ continue;
85
+ }
86
+ if (word.startsWith('-'))
87
+ continue;
88
+ const found = spec(word);
89
+ if (found)
90
+ packages.push(found);
91
+ }
92
+ return { packages, from };
93
+ }
94
+ // R code's install calls: install.packages("a"), install.packages(c("a", "b")), BiocManager::install(...).
95
+ function rInstalls(code) {
96
+ const found = [];
97
+ for (const match of code.matchAll(/\b(?:(utils)::)?(install\.packages|(BiocManager|pak|remotes|renv)::(?:install|pkg_install|install_github|install_cran))\s*\(([^()]*(?:\([^()]*\)[^()]*)*)\)/g)) {
98
+ const names = [...(match[4] ?? '').matchAll(/["']([A-Za-z0-9._/@-]+)["']/g)].map(m => m[1]).filter(name => !/^(https?|cran|repos)/i.test(name));
99
+ if (names.length)
100
+ found.push({ tool: match[3] ?? 'R', names });
101
+ }
102
+ return found;
103
+ }
104
+ export function readInstall(command, printed) {
105
+ let tool = null, from = null;
106
+ const packages = [];
107
+ for (const raw of commands(command)) {
108
+ let words = raw.slice();
109
+ while (words.length && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0]) || ['rtk', 'proxy', 'sudo', 'env', 'command', 'exec', 'time', 'nohup', '{', '}', '!', 'then', 'else', 'do'].includes(words[0])))
110
+ words = words.slice(1);
111
+ const head = base(words[0] ?? ''), rest = words.slice(1);
112
+ let args = null, by = null;
113
+ if (head === 'uv' && rest[0] === 'pip' && rest[1] === 'install') {
114
+ args = rest.slice(2);
115
+ by = 'uv';
116
+ }
117
+ else if (head === 'uv' && rest[0] === 'add') {
118
+ args = rest.slice(1);
119
+ by = 'uv';
120
+ }
121
+ else if (/^pip3?(\.\d+)?$/.test(head) && rest[0] === 'install') {
122
+ args = rest.slice(1);
123
+ by = 'pip';
124
+ }
125
+ else if (/^python(\d(\.\d+)?)?$/.test(head) && rest[0] === '-m' && rest[1] === 'pip' && rest[2] === 'install') {
126
+ args = rest.slice(3);
127
+ by = 'pip';
128
+ }
129
+ else if (/^(conda|mamba|micromamba)$/.test(head) && rest[0] === 'install') {
130
+ args = rest.slice(1);
131
+ by = head;
132
+ }
133
+ else if (/^(Rscript|R)$/.test(head)) {
134
+ const code = rest.filter((_, i) => rest[i - 1] === '-e' || rest[i - 1] === '--expr').join('\n');
135
+ for (const call of rInstalls(code)) {
136
+ by ??= call.tool;
137
+ for (const name of call.names)
138
+ packages.push({ name: name.split('/').pop().split('@')[0], version: null });
139
+ }
140
+ if (by)
141
+ tool ??= by;
142
+ continue;
143
+ }
144
+ if (!args || !by)
145
+ continue;
146
+ tool ??= by;
147
+ const read = pythonArgs(args);
148
+ packages.push(...read.packages);
149
+ from ??= read.from;
150
+ }
151
+ if (!tool || !packages.length && !from)
152
+ return null;
153
+ // What was installed, as the tool printed it: pip's "Successfully installed a-1.0 b-2.0" and
154
+ // uv's " + a==1.0" lines. Names compare as package indexes compare them.
155
+ const key = (name) => name.toLowerCase().replace(/[-_.]+/g, '-');
156
+ const versions = new Map();
157
+ for (const line of (printed ?? '').split('\n')) {
158
+ const pip = line.match(/Successfully installed (.+)$/);
159
+ if (pip)
160
+ for (const item of pip[1].trim().split(/\s+/)) {
161
+ const at = item.lastIndexOf('-');
162
+ if (at > 0)
163
+ versions.set(key(item.slice(0, at)), item.slice(at + 1));
164
+ }
165
+ const uv = line.match(/^\s*\+\s+([A-Za-z0-9][A-Za-z0-9._-]*)==(\S+)/);
166
+ if (uv)
167
+ versions.set(key(uv[1]), uv[2]);
168
+ }
169
+ const named = new Set(packages.map(p => key(p.name)));
170
+ const seen = new Set(), listed = packages.filter(p => !seen.has(key(p.name)) && seen.add(key(p.name)))
171
+ .map(p => ({ name: p.name, version: versions.get(key(p.name)) ?? p.version }));
172
+ return { tool, packages: listed, more: [...versions.keys()].filter(name => !named.has(name)).length, from };
173
+ }
174
+ // "pandas 2.2.3, statsmodels 0.15.0 and 6 more (uv)", or "packages from requirements.txt (pip)".
175
+ export function installText(installed) {
176
+ const names = installed.packages.map(p => p.version ? `${p.name} ${p.version}` : p.name);
177
+ const what = names.length ? names.join(', ') : `packages from ${installed.from}`;
178
+ return `${what}${installed.more ? ` and ${installed.more} more` : ''} (${installed.tool})`;
179
+ }
@@ -21,6 +21,7 @@ export function notesInstructions(conversationId, options = {}) {
21
21
  '- Data & method: one bullet for the data and one for each script, each linked, saying what it is and how it is used.',
22
22
  '- Key results: one bullet per finding, each linking the result files it rests on; embed a key figure on its own line as ![what it shows](path/to/figure.png).',
23
23
  '- Files: right after you write a result, table, figure or script, and before your next command, add a bullet as - [path](path) — what it holds; correct it when that changes. Do not leave this to the end.',
24
+ '- Scripts and cells: above each top-level statement that first makes a value you report, or writes a file, add a comment `# means: …` with one short sentence saying what it is, in the language the person writes in, under 200 characters. Where one statement makes or writes several things, name the one the comment is for: `# means NAME: …`, with a value name or a file path. Comments inside a function, and changes to a value after it is made, need none. Rho shows these sentences beside the code, so a reader who does not read R or Python can follow the steps.',
24
25
  '- Choices: when the analysis takes a choice that has a reasonable alternative (a test, a threshold, an exclusion, a model), go on with your choice and do not ask the person or wait. Where the alternative is cheap, compute it too. Write one bullet saying what you chose and what it gives, linking its results, with a nested bullet starting "Alternative:" that says what the other option gives, linking its results.',
25
26
  '- To do: a task list (- [ ] open, - [x] done).',
26
27
  '- Leave a subsection empty until you have something for it; do not write placeholders such as "to be filled".',
@@ -33,6 +34,7 @@ export function notesInstructions(conversationId, options = {}) {
33
34
  '- What a cell defines stays for your next cells, so load data once and build on it. kernel.read shows the cells, including any the person ran, and the variables.',
34
35
  '- To look at a value without adding a cell, call kernel.inspect with its language and name: it gives a page of a table, a model\'s fit and coefficients, or a list\'s parts, with the code that reaches each part.',
35
36
  '- Keep the code your results rest on in script files as well, so they can be run again without the session.',
37
+ '- Before a multi-step analysis, read the Skill "rho.analysis-run" with skill.list and skill.read: it holds the practice that keeps a long analysis from losing work when a turn stops, written after five real reproductions ran here.',
36
38
  ] : []),
37
39
  ].join('\n');
38
40
  }
package/dist/protocol.js CHANGED
@@ -20,7 +20,9 @@ const array = (items) => ({ type: 'array', items });
20
20
  const response = (properties, required = Object.keys(properties)) => ({ type: 'object', properties, required, additionalProperties: true });
21
21
  const changeId = { type: 'string', pattern: '^[A-Za-z0-9._:@-]{1,128}$' };
22
22
  const identity = changeId;
23
- const agentBackend = { enum: ['claude-acp', 'codex-acp'] };
23
+ // 'acp' is a generic ACP agent: the operator names its command (--acp-command), so Rho
24
+ // supports any ACP v1 implementation without a per-agent adapter.
25
+ const agentBackend = { enum: ['claude-acp', 'codex-acp', 'acp'] };
24
26
  const outsideAgent = { enum: ['claude-code', 'codex'] };
25
27
  const addedTo = response({ agent: outsideAgent, name: text, scope: { enum: ['folder', 'user'] }, at: text });
26
28
  const choiceText = { type: 'string', minLength: 1, maxLength: 512 }, choiceValue = { anyOf: [choiceText, { type: 'boolean' }] };
@@ -81,7 +83,7 @@ export const operations = { ...base.operations,
81
83
  // The person's Agent and its settings (model, effort, mode…) among those the Agent offers.
82
84
  'conversation.configure': obj({ ...project, conversationId: changeId, configId: choiceText, value: choiceValue }),
83
85
  'conversation.settings.read': obj(project),
84
- 'conversation.settings.update': obj({ ...project, expectedVersion: integer, backend: { anyOf: [agentBackend, { type: 'null' }] }, choices: obj({ 'claude-acp': choices, 'codex-acp': choices }, []), describe: obj({ enabled: { type: 'boolean' } }, ['enabled']) }, ['projectId', 'expectedVersion']),
86
+ 'conversation.settings.update': obj({ ...project, expectedVersion: integer, backend: { anyOf: [agentBackend, { type: 'null' }] }, choices: obj({ 'claude-acp': choices, 'codex-acp': choices, 'acp': choices }, []), describe: obj({ enabled: { type: 'boolean' } }, ['enabled']) }, ['projectId', 'expectedVersion']),
85
87
  'conversation.settings.check': obj({ ...project, backend: agentBackend }),
86
88
  // Claude Code or Codex running outside Rho, given this project and told how to start Rho's MCP server.
87
89
  'session.connect': obj({ ...project, agent: outsideAgent })
@@ -201,8 +203,8 @@ const conversationSummary = response({ conversationId: text, title: text, client
201
203
  const sessionObservation = ownerSchema('agent-session', 'sessionObservationSchema');
202
204
  // What one Agent offered, from a check or a conversation's first session, and the person's choices.
203
205
  const offered = response({ at: text, source: { enum: ['check', 'conversation'] }, adapter: { anyOf: [response({ name: text, version: text }, ['name']), { type: 'null' }] }, options: array(ownerSchema('agent-session', 'configOptionSchema')), omitted: integer });
204
- const agentSettings = response({ version: integer, updatedAt: { anyOf: [text, { type: 'null' }] }, backend: { anyOf: [agentBackend, { type: 'null' }] }, serviceBackend: { enum: ['claude-acp', 'codex-acp', 'claude-cli'] },
205
- choices: response({ 'claude-acp': choices, 'codex-acp': choices }, []), offered: response({ 'claude-acp': offered, 'codex-acp': offered }, []), checking: array(agentBackend),
206
+ const agentSettings = response({ version: integer, updatedAt: { anyOf: [text, { type: 'null' }] }, backend: { anyOf: [agentBackend, { type: 'null' }] }, serviceBackend: { enum: ['claude-acp', 'codex-acp', 'claude-cli', 'acp'] },
207
+ choices: response({ 'claude-acp': choices, 'codex-acp': choices, 'acp': choices }, []), offered: response({ 'claude-acp': offered, 'codex-acp': offered, 'acp': offered }, []), checking: array(agentBackend),
206
208
  // Whether a model describes the files a message writes; the facts are read either way.
207
209
  describe: response({ enabled: { type: 'boolean' } }) }, ['version', 'updatedAt', 'backend', 'serviceBackend', 'choices', 'offered', 'checking']);
208
210
  // A tool step keeps its original identity, the Agent's bounded human title, its ACP
@@ -405,7 +407,7 @@ export const descriptions = {
405
407
  'console.events': 'Who ran each Console command and when, newest first, for the timeline: the folder, how it ended and its first line. No output.',
406
408
  'console.output': 'One bounded page of what a Console command printed. Output past the kept size is counted in a note, not silently dropped.',
407
409
  'console.stop': 'Person at this machine only: ask a running Console command and the processes it started to stop; they are killed after a short grace period.',
408
- 'kernel.run': 'Run a cell of R or Python in the project\'s live session of that language, which the Agent and the person share: what one cell defines stays for the next. Starts the session when none is live, on this machine or at a place on one of the person\'s servers (place, or where kernel.places says that language runs next). The same changeId from the same caller returns the same cell and never runs it twice. waitMs waits for the cell to end.',
410
+ 'kernel.run': 'Run a cell of R or Python in the project\'s live session of that language, which the Agent and the person share: what one cell defines stays for the next. Starts the session when none is live, on this machine or at a place on one of the person\'s servers (place, or where kernel.places says that language runs next). interpreter names the program a session on this machine starts with, in a folder of the project such as .venv/bin/python; the project keeps it for that language\'s later sessions, and each cell records the program and version that ran it. The same changeId from the same caller returns the same cell and never runs it twice. waitMs waits for the cell to end.',
409
411
  'skills.list': 'The project\'s skills: how work is done here (the methods the person chose, the conventions they keep, the traps its work ran into), each a SKILL.md in .claude/skills that every Agent here reads, with what it is for and its newest change. Read before analysing.',
410
412
  'skills.read': 'One project skill\'s text and its changes, newest first: who made each, why, from what (cells, conversations, files) and the lines it added and removed, and what changed outside Rho since.',
411
413
  'skills.change': 'Record in a project skill when the person corrects a method or says how they work, or a failure\'s cause and fix are found: a new skill, a new text with a one-sentence reason and evidence, or its removal. Record how to work, never results. Made on the version read; the same changeId never writes twice.',
@@ -0,0 +1,2 @@
1
+ import { SkillDeclaration } from '@rho-workspace/plugin-contracts';
2
+ export declare const screenCheckSkill: SkillDeclaration;
@@ -0,0 +1,39 @@
1
+ import { digest } from '@rho-workspace/plugin-contracts';
2
+ // The practice for checking a page's layout in a real browser, served as the Skill rho.screen-check.
3
+ // skills/rho/references/screen-check.md carries this text verbatim, and
4
+ // test/screen-check-skill.test.mjs fails when the two differ, so the text has one source.
5
+ const screenCheckContent = [
6
+ 'Check how a page lays out in a real browser before you say it works.',
7
+ '',
8
+ 'Run `rho-screen-check <url or page.html>`. It opens the page at each viewport (by default 1440x900, 1024x768, 768x1024 and 390x844), waits for it to settle, writes one screenshot per viewport and prints a JSON report. Options: `--viewport WxH` (repeatable), `--out DIR` (default ./screen-check), `--overlap SELECTOR` (report the visible boxes of that selector that overlap one another), `--full-page`, `--settle MS` (default 800), `--browser auto|chrome|msedge|chromium|PATH` (default auto: Chrome, then Edge, then Playwright\'s Chromium).',
9
+ '',
10
+ 'Read the exit status first. 0: the check ran, and its findings are in the report. 1: an argument was wrong. 2: the check did not run; `error.kind`, `error.message` and `error.next` say why and what to do. Do what `next` says, then run it again.',
11
+ '',
12
+ 'For each viewport, read: `horizontalOverflow` with `scrollWidth` against `clientWidth` (the page is wider than the window); `elementsPastEdgeUnclipped` (what crosses the right edge and no box keeps in: read this count first), then `elementsPastEdge` (the first 50, unclipped first; `clippedBy` names the box that scrolls or hides what is beyond its own width); `unnamedButtons`; `overlaps` for your selector; `pageErrors` and `consoleErrors`; `httpErrors` (each response that failed, with its status and address); `requestsOutsideTarget` (what the page loads from elsewhere).',
13
+ '',
14
+ 'Then open each `screenshot` with the Read tool and say what you see. The report says what the page does, not whether a person can follow it; the screenshot and the flow answer that. Check the states a person reaches as well as the first screen: an empty state, an error, a long name or number, a narrow window, the screen after a step. Run the check again after each change.',
15
+ '',
16
+ 'Pitfalls seen in real runs:',
17
+ '- A selector can match hidden elements. The report counts visible boxes only; your own probes must filter by visibility too.',
18
+ '- The same attribute can appear in two views at once, such as a script editor\'s flow and a session\'s Data flow. Open a workspace that shows only the view you check.',
19
+ '- At 390 px the sidebar collapses and project labels are hidden. Reach the label through the page\'s own controls before you look for it.',
20
+ '- Text cut short inside a box of fixed height, such as a label that ends mid-word, is not in the report. Only the screenshot shows it.',
21
+ '- Restarting Rho ends the live R sessions, and the page then shows "Rho closed. Values defined before this are gone." Say so when a check follows a restart.',
22
+ '- Check a copy of the project, not the person\'s own project. A check must not change what the person has.',
23
+ ].join('\n');
24
+ export const screenCheckSkill = {
25
+ id: 'rho.screen-check',
26
+ version: '1.0',
27
+ content: screenCheckContent,
28
+ digest: digest(screenCheckContent),
29
+ intent: 'Check how a Rho screen or a local page lays out at phone and desktop widths, in a real browser, and read the result rather than assume it.',
30
+ failureRecipes: [
31
+ { kind: 'browser_missing', message: 'No browser could be started.', next: 'Install Google Chrome or Microsoft Edge, or pass --browser with the path of an installed Chromium-based browser; then run the check again.' },
32
+ { kind: 'unreachable', message: 'The page did not load.', next: 'Check the address, that the service is running or the file exists, and that the page opens in a browser; then run the check again.' },
33
+ { kind: 'usage', message: 'An argument was not understood.', next: 'Run rho-screen-check --help and use one of the listed forms.' },
34
+ ],
35
+ verify: {
36
+ command: 'npm run test:screen-check',
37
+ description: 'Runs the screen-check tests: the findings on fixture pages in a real browser, the JSON report and the exit codes, and this Skill text against the shipped reference.',
38
+ },
39
+ };
package/dist/server.d.ts CHANGED
@@ -70,6 +70,7 @@ export interface ServiceConfig extends Partial<typeof serviceLimits>, Partial<ty
70
70
  claude?: string;
71
71
  agentBackend?: ConversationConfig['backend'];
72
72
  claudeAcp?: ConversationConfig['claudeAcp'];
73
+ acpCommand?: ConversationConfig['acpCommand'];
73
74
  codex?: string;
74
75
  codexAcp?: ConversationConfig['codexAcp'];
75
76
  conversations?: Partial<typeof conversationLimits>;
@@ -160,6 +161,7 @@ export declare class RhoService {
160
161
  private catalog;
161
162
  private modelFacts;
162
163
  start(): Promise<this>;
164
+ private followAddress;
163
165
  agentConnection(projectId: string, externalSessionId: string, file: string, methods?: string[], pluginFilter?: string[], profile?: 'layered-business' | 'layered-presentation', who?: {
164
166
  label?: string;
165
167
  conversationId?: string;
package/dist/server.js CHANGED
@@ -385,7 +385,7 @@ export class RhoService {
385
385
  // The same project grant `rho --grant` gives, under this conversation's own principal.
386
386
  grant: async (project, conversation) => {
387
387
  const session = 'rho:' + conversation.id, file = path.join(this.state.directory, 'agent-' + digest(project.id + ':' + session) + '.json');
388
- await this.agentConnection(project.id, session, file, undefined, undefined, 'layered-business', { label: (conversation.client === 'codex' ? 'Codex' : 'Claude Code') + ' in Rho', conversationId: conversation.id });
388
+ await this.agentConnection(project.id, session, file, undefined, undefined, 'layered-business', { label: (conversation.client === 'codex' ? 'Codex' : conversation.client === 'acp' ? 'ACP agent' : 'Claude Code') + ' in Rho', conversationId: conversation.id });
389
389
  return { file, principal: 'agent:' + digest(project.id + ':' + session).slice(0, 32) };
390
390
  },
391
391
  directory: project => this.dataDir(project),
@@ -462,7 +462,7 @@ export class RhoService {
462
462
  return { text: said.text, model: said.model, cost: callCost(said, listed ? await this.modelFacts(listed).catch(() => null) : null) };
463
463
  }
464
464
  } : {}
465
- }, config.claude, config.conversations, { backend: config.agentBackend, claudeAcp: config.claudeAcp, codex: config.codex, codexAcp: config.codexAcp, settingsFile: path.join(this.state.directory, 'agent-settings.json') });
465
+ }, config.claude, config.conversations, { backend: config.agentBackend, claudeAcp: config.claudeAcp, acpCommand: config.acpCommand, codex: config.codex, codexAcp: config.codexAcp, settingsFile: path.join(this.state.directory, 'agent-settings.json') });
466
466
  this.conversations.describing = { ...describeLimits, ...config.describe };
467
467
  this.folderPicker = new NativeFolderPicker({ ...nativeFolderLimits, ...config.nativeFolders });
468
468
  this.server = http.createServer((req, res) => void this.handle(req, res));
@@ -501,9 +501,33 @@ export class RhoService {
501
501
  const chosen = (earlier?.selectedProjects ?? []).filter(id => this.state.projects.some(p => p.id === id && p.nativeSelected));
502
502
  this.state.principals = this.state.principals.filter(p => p.id !== localBrowserId);
503
503
  await this.state.addPrincipal({ ...localBrowserPrincipal(await fs.realpath(this.config.root)), ...(chosen.length ? { selectedProjects: chosen } : {}) });
504
- await new Promise((resolve, reject) => { this.server.once('error', reject); this.server.listen(this.config.port ?? 0, '127.0.0.1', () => resolve()); });
504
+ // A person's bookmark and every Agent connection name this service's address. A start that
505
+ // selects no port takes the one this state's last start had, and a new one only when that
506
+ // port is taken now; a selected port is used or refused, never replaced.
507
+ const connectionFile = path.join(this.state.directory, 'connection.json');
508
+ const earlierUrl = await fs.readFile(connectionFile, 'utf8').then(text => String(JSON.parse(text).url), () => '');
509
+ const earlierPort = /^http:\/\/127\.0\.0\.1:\d+$/.test(earlierUrl) ? Number(new URL(earlierUrl).port) : 0;
510
+ const listen = (port) => new Promise((resolve, reject) => {
511
+ this.server.once('error', reject);
512
+ this.server.listen(port, '127.0.0.1', () => { this.server.off('error', reject); resolve(); });
513
+ });
514
+ try {
515
+ await listen(this.config.port ?? earlierPort);
516
+ }
517
+ catch (error) {
518
+ const taken = error.code === 'EADDRINUSE';
519
+ requireThat(!taken || this.config.port === undefined, 'conflict', `Port ${this.config.port} on 127.0.0.1 is in use by another program. If that is Rho, open http://127.0.0.1:${this.config.port}; otherwise choose another --port, or leave it out.`, { port: this.config.port });
520
+ if (!taken)
521
+ throw error;
522
+ await listen(0);
523
+ }
505
524
  this.url = 'http://127.0.0.1:' + this.server.address().port;
506
- await atomicJson(path.join(this.state.directory, 'connection.json'), { url: this.url, token: this.adminToken, apiVersion: '2.0' });
525
+ await atomicJson(connectionFile, { url: this.url, token: this.adminToken, apiVersion: '2.0' });
526
+ // An Agent's connection holds the address it was given. When the address changed, each one
527
+ // that named the earlier address follows, so the Agent's next session reaches this service
528
+ // with the grant it already has. Its token and methods are unchanged.
529
+ if (earlierUrl && earlierUrl !== this.url)
530
+ await this.followAddress(earlierUrl);
507
531
  return this;
508
532
  }
509
533
  catch (e) {
@@ -513,6 +537,20 @@ export class RhoService {
513
537
  throw e;
514
538
  }
515
539
  }
540
+ // Only this state's own connection files, and only those that named the earlier address: a
541
+ // file that cannot be read or rewritten keeps what it had, and the start goes on.
542
+ async followAddress(earlierUrl) {
543
+ const names = await fs.readdir(this.state.directory).catch(() => []);
544
+ for (const name of names.filter(name => /^agent-[0-9a-f]+\.json$/.test(name))) {
545
+ const file = path.join(this.state.directory, name);
546
+ try {
547
+ const connection = JSON.parse(await fs.readFile(file, 'utf8'));
548
+ if (connection?.url === earlierUrl && typeof connection.token === 'string')
549
+ await atomicJson(file, { ...connection, url: this.url });
550
+ }
551
+ catch { /* left as it was */ }
552
+ }
553
+ }
516
554
  async agentConnection(projectId, externalSessionId, file, methods, pluginFilter, profile = 'layered-business', who = {}) {
517
555
  requireThat(profile === undefined || ['layered-business', 'layered-presentation'].includes(profile), 'invalid', 'Choose an explicitly supported layered profile.');
518
556
  const project = await this.state.project(projectId, this.state.principals.find(p => p.id === 'operator'));
@@ -952,7 +990,11 @@ export class RhoService {
952
990
  add('rscript', rscriptAvailable ? 'ok' : b.intent === 'run-r' ? 'fail' : 'warn', rscriptAvailable ? `R is available at ${this.bridge.rscript}.` : `The optional managed R entry is unavailable at ${this.bridge.rscript}. Native Agent tools remain available.`, rscriptAvailable ? '' : 'For managed R scripts, select an existing Rscript with --rscript or RHO_RSCRIPT.');
953
991
  // Conversations drive the person's installed Claude Code or Codex; the package carries none.
954
992
  const agentCommand = await import('@rho-workspace/plugin-agent/acp-command').catch(() => null);
955
- if (agentCommand) {
993
+ if (this.config.agentBackend === 'acp') {
994
+ const cmd = this.config.acpCommand;
995
+ add('agents', cmd?.file ? 'ok' : 'warn', cmd?.file ? `A generic ACP agent is configured (${cmd.file} ${cmd.args.join(' ')}).` : 'No ACP agent command is configured.', cmd?.file ? '' : 'Start Rho with --agent-backend acp --acp-command <argv> or RHO_ACP_COMMAND.');
996
+ }
997
+ else if (agentCommand) {
956
998
  const claude = this.config.claude ?? process.env.RHO_CLAUDE, codex = this.config.codex ?? process.env.RHO_CODEX;
957
999
  const env = { ...process.env, ...(claude ? { CLAUDE_CODE_EXECUTABLE: claude } : {}), ...(codex ? { CODEX_PATH: codex } : {}) };
958
1000
  const preferred = this.config.agentBackend === 'codex-acp' ? 'codex' : 'claude', other = preferred === 'codex' ? 'claude' : 'codex';
@@ -1083,8 +1125,9 @@ export class RhoService {
1083
1125
  }
1084
1126
  // The person's own Claude Code or Codex, outside Rho: the same grant the operator gives, kept
1085
1127
  // when it already exists so a connected session keeps its token, then the Agent's own `mcp add`.
1128
+ // The person does it from the page; whoever started the service, with `rho --grant --agent`.
1086
1129
  if (method === 'session.connect') {
1087
- requireThat(principal.id === localBrowserId, 'unauthorized', 'Only the person at this machine can add Rho to their own Agent.');
1130
+ requireThat(principal.id === localBrowserId || principal.id === 'operator', 'unauthorized', 'Only the person at this machine can add Rho to their own Agent.');
1088
1131
  const agent = b.agent, file = path.join(this.state.directory, 'agent-' + digest(p.id + ':' + agent) + '.json');
1089
1132
  const id = 'agent:' + digest(p.id + ':' + agent).slice(0, 32), existing = this.state.principals.find(x => x.id === id);
1090
1133
  // A grant is kept while its connection still reaches this service with its token; otherwise
@@ -1184,7 +1227,7 @@ export class RhoService {
1184
1227
  await capabilities.assertAvailable(method, principal);
1185
1228
  const host = this.kernelFor(p);
1186
1229
  if (method === 'kernel.run')
1187
- return host.run({ language: b.language, code: b.code, purpose: b.purpose, changeId: b.changeId, by: principal.id, waitMs: b.waitMs, place: b.place });
1230
+ return host.run({ language: b.language, code: b.code, purpose: b.purpose, changeId: b.changeId, by: principal.id, waitMs: b.waitMs, place: b.place, interpreter: b.interpreter });
1188
1231
  if (method === 'kernel.read')
1189
1232
  return host.read(b.limit, b.cellId);
1190
1233
  if (method === 'kernel.events')