@rho-workspace/rho 0.5.6 → 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 (211) hide show
  1. package/dist/agent-connect.d.ts +36 -2
  2. package/dist/agent-connect.js +64 -11
  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 +76 -11
  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 +1191 -278
  15. package/dist/contract.generated.js +1 -1
  16. package/dist/contracts/owners.generated.js +845 -47
  17. package/dist/conversation-presentation.d.ts +9 -2
  18. package/dist/conversation-presentation.js +3 -1
  19. package/dist/conversations.d.ts +154 -8
  20. package/dist/conversations.js +147 -24
  21. package/dist/discovery.js +2 -2
  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 +6 -0
  40. package/dist/protocol.js +30 -12
  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 +2 -0
  46. package/dist/server.js +71 -8
  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 +133 -74
  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-Trqc0swD.js → console-Dyuo1q4N.js} +4 -4
  60. package/dist/web/assets/{dist-pF902jHA.js → dist-CSiCyBml.js} +1 -1
  61. package/dist/web/assets/{dist-DpIHRk3B.js → dist-D9FLb9Hm.js} +1 -1
  62. package/dist/web/assets/{dist-eMx6b8N1.js → dist-FXbTBwML.js} +1 -1
  63. package/dist/web/assets/{dist-DZmszrjG.js → dist-l3DUhbw5.js} +1 -1
  64. package/dist/web/assets/{explore-in-BCHqjJ7B.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-DcVGUBBY.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-DYP0aCXb.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-1ec0Z-jx.js → skills-Dyz7mBlc.js} +1 -1
  85. package/dist/web/assets/{skills-work-DNexV4OJ.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-COmCbGV3.js → workspace-DfjBL--r.js} +2 -2
  90. package/dist/web/composition.json +138 -83
  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 +53 -11
  99. package/docs/HEADLESS.md +104 -11
  100. package/docs/THIRD-PARTY-PLUGINS.md +75 -3
  101. package/node_modules/@rho-workspace/plugin-agent/README.md +5 -0
  102. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +31 -0
  103. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +45 -10
  104. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +19 -3
  105. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +3 -2
  106. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +1 -1
  107. package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +8 -0
  108. package/node_modules/@rho-workspace/plugin-agent/dist/index.js +3 -2
  109. package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.d.ts +13 -0
  110. package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.js +13 -0
  111. package/node_modules/@rho-workspace/plugin-agent/dist/mcp.js +16 -8
  112. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +1 -1
  113. package/node_modules/@rho-workspace/plugin-agent/package.json +1 -1
  114. package/node_modules/@rho-workspace/plugin-figures/LICENSE +661 -0
  115. package/node_modules/@rho-workspace/plugin-figures/README.md +191 -0
  116. package/node_modules/@rho-workspace/plugin-figures/dist/index.d.ts +236 -0
  117. package/node_modules/@rho-workspace/plugin-figures/dist/index.js +165 -0
  118. package/node_modules/@rho-workspace/plugin-figures/dist/operations.d.ts +44 -0
  119. package/node_modules/@rho-workspace/plugin-figures/dist/operations.js +39 -0
  120. package/node_modules/@rho-workspace/plugin-figures/dist/presentation.d.ts +19 -0
  121. package/node_modules/@rho-workspace/plugin-figures/dist/presentation.js +26 -0
  122. package/node_modules/@rho-workspace/plugin-figures/dist/record.d.ts +188 -0
  123. package/node_modules/@rho-workspace/plugin-figures/dist/record.js +143 -0
  124. package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.d.ts +1 -0
  125. package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.js +68 -0
  126. package/node_modules/@rho-workspace/plugin-figures/dist/ui.d.ts +56 -0
  127. package/node_modules/@rho-workspace/plugin-figures/dist/ui.js +363 -0
  128. package/node_modules/@rho-workspace/plugin-figures/package.json +41 -0
  129. package/node_modules/@rho-workspace/plugin-figures/py/figures.py +856 -0
  130. package/node_modules/@rho-workspace/plugin-figures/r/figures.R +775 -0
  131. package/node_modules/@rho-workspace/plugin-kernel/README.md +27 -3
  132. package/node_modules/@rho-workspace/plugin-kernel/dist/contract.d.ts +12 -0
  133. package/node_modules/@rho-workspace/plugin-kernel/dist/contract.js +8 -1
  134. package/node_modules/@rho-workspace/plugin-kernel/dist/index.d.ts +61 -0
  135. package/node_modules/@rho-workspace/plugin-kernel/dist/index.js +125 -9
  136. package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.d.ts +14 -0
  137. package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.js +9 -0
  138. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-base.d.ts +23 -5
  139. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook-work.js +1 -1
  140. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.d.ts +6 -4
  141. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.js +44 -17
  142. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-styles.js +6 -0
  143. package/node_modules/@rho-workspace/plugin-kernel/package.json +1 -1
  144. package/node_modules/@rho-workspace/plugin-kernel/runners/runner.R +144 -4
  145. package/node_modules/@rho-workspace/plugin-kernel/runners/runner.py +106 -1
  146. package/node_modules/@rho-workspace/plugin-tables/README.md +5 -0
  147. package/node_modules/@rho-workspace/plugin-tables/dist/index.d.ts +1 -0
  148. package/node_modules/@rho-workspace/plugin-tables/dist/index.js +19 -3
  149. package/node_modules/@rho-workspace/plugin-tables/dist/ui.js +1 -1
  150. package/node_modules/@rho-workspace/plugin-tables/package.json +1 -1
  151. package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +3 -2
  152. package/node_modules/jose/dist/webapi/jwks/remote.js +1 -1
  153. package/node_modules/jose/dist/webapi/jws/general/verify.js +2 -8
  154. package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +11 -8
  155. package/node_modules/jose/dist/webapi/lib/key_management.js +0 -1
  156. package/node_modules/jose/package.json +1 -1
  157. package/package.json +6 -4
  158. package/protocol/index.json +91 -55
  159. package/protocol/operations/agent.sessions.list.json +13 -5
  160. package/protocol/operations/agent.sessions.read.json +13 -5
  161. package/protocol/operations/capability.list.json +382 -6
  162. package/protocol/operations/composition.apply.json +10 -2
  163. package/protocol/operations/composition.delete.json +845 -0
  164. package/protocol/operations/composition.read.json +10 -2
  165. package/protocol/operations/composition.undo.json +10 -2
  166. package/protocol/operations/conversation.agents.add.json +346 -0
  167. package/protocol/operations/conversation.agents.remove.json +340 -0
  168. package/protocol/operations/conversation.decide.json +196 -0
  169. package/protocol/operations/conversation.permission.json +196 -0
  170. package/protocol/operations/conversation.read.json +196 -0
  171. package/protocol/operations/conversation.send.json +265 -0
  172. package/protocol/operations/conversation.settings.check.json +213 -495
  173. package/protocol/operations/conversation.settings.read.json +211 -490
  174. package/protocol/operations/conversation.settings.update.json +235 -554
  175. package/protocol/operations/conversation.start.json +267 -5
  176. package/protocol/operations/conversation.stop.json +196 -0
  177. package/protocol/operations/extension.describe.json +39 -0
  178. package/protocol/operations/extension.install.json +62 -0
  179. package/protocol/operations/extension.list.json +164 -1
  180. package/protocol/operations/extension.market.change.json +210 -0
  181. package/protocol/operations/figure.read.json +265 -0
  182. package/protocol/operations/figure.select.json +157 -0
  183. package/protocol/operations/kernel.interrupt.json +119 -0
  184. package/protocol/operations/kernel.read.json +119 -0
  185. package/protocol/operations/kernel.run.json +119 -0
  186. package/protocol/operations/observation.snapshot.json +416 -8
  187. package/protocol/operations/project.openFile.json +10 -2
  188. package/protocol/operations/session.grant.json +4 -0
  189. package/protocol/operations/shared.json +1 -0
  190. package/protocol/operations/view.close.json +10 -2
  191. package/protocol/operations/view.protection.set.json +10 -2
  192. package/vendor/composition.json +177 -112
  193. package/vendor/rho-workspace-plugin-agent-0.4.33.tgz +0 -0
  194. package/vendor/rho-workspace-plugin-figures-0.5.1.tgz +0 -0
  195. package/vendor/rho-workspace-plugin-kernel-0.5.13.tgz +0 -0
  196. package/vendor/rho-workspace-plugin-tables-0.5.2.tgz +0 -0
  197. package/web/theme.css +5 -0
  198. package/dist/web/assets/answer-board-CBkGbSLD.js +0 -1
  199. package/dist/web/assets/canvas-D8rDdfDz.js +0 -1
  200. package/dist/web/assets/choices-D-YfnFP6.js +0 -1
  201. package/dist/web/assets/graph-lENgfQNH.js +0 -2
  202. package/dist/web/assets/index-B3_OVy91.css +0 -1
  203. package/dist/web/assets/index-BpL_Kl_s.js +0 -892
  204. package/dist/web/assets/overview-CDnVKlOt.js +0 -6
  205. package/dist/web/assets/preview-BEC1E9MK.js +0 -1
  206. package/dist/web/assets/studio-process-EYQgikxq.js +0 -2
  207. package/dist/web/assets/timeline-events-Chj2Gbx7.js +0 -1
  208. package/dist/web/assets/work-turns-CT9RVcWY.js +0 -1
  209. package/vendor/rho-workspace-plugin-agent-0.4.30.tgz +0 -0
  210. package/vendor/rho-workspace-plugin-kernel-0.5.5.tgz +0 -0
  211. package/vendor/rho-workspace-plugin-tables-0.5.0.tgz +0 -0
@@ -1,5 +1,6 @@
1
1
  import { projectPath, commandFiles, buildWorkGraph } from './work-graph.js';
2
2
  import { notesInstructions, projectNotesPath } from './project-notes.js';
3
+ import { uiContextPrompt } from './ui-context.js';
3
4
  import { addSpend, describeLimits, describable, fileFacts, figureContext, raster } from './file-describer.js';
4
5
  import { agentTurnLimit, displayCommand, toolActivity } from './conversation-presentation.js';
5
6
  import { privateEvidencePath } from './evidence-capture.js';
@@ -8,7 +9,7 @@ import path from 'node:path';
8
9
  import { createHash, randomBytes, randomUUID } from 'node:crypto';
9
10
  import { fileURLToPath } from 'node:url';
10
11
  import { atomicJson, canonical, digest, Fault, now, readJson, requireThat } from '@rho-workspace/plugin-contracts';
11
- import { acpEnvironment, missingClient } from '@rho-workspace/plugin-agent/acp-command';
12
+ import { acpEnvironment, installedAcpAgents, knownAcpAgent, locate, missingClient } from '@rho-workspace/plugin-agent/acp-command';
12
13
  // Rho holds user messages and their original identities. The selected Agent plugin
13
14
  // owns the connection, native session and protocol. Reads never start a driver.
14
15
  // A new Claude Code session opens in Manual mode unless the person's Claude settings name another.
@@ -16,6 +17,9 @@ import { acpEnvironment, missingClient } from '@rho-workspace/plugin-agent/acp-c
16
17
  // settings stays, and a model without Auto falls back to Accept edits with the adapter's warning.
17
18
  const claudePreferred = { mode: { value: 'auto', replaces: ['default'] } };
18
19
  export const conversationLimits = {
20
+ // Per-message interface observations: omit and count extra views; refuse an
21
+ // observation still larger than the byte budget before sending the message.
22
+ maxUiContextBytes: 16384, maxUiContextViews: 32,
19
23
  // Conversations kept per project, not counting the trash; starting or restoring another is
20
24
  // refused. The trash keeps as many again; past that, moving one there is refused.
21
25
  maxConversations: 64,
@@ -56,6 +60,9 @@ export const conversationLimits = {
56
60
  // Settings chosen for one Agent or one conversation, and the length of one chosen value;
57
61
  // more are refused.
58
62
  maxChoices: 12, maxChoiceChars: 512,
63
+ // ACP Agents the person adds in Rho, kept in this service's Agent settings and offered in every
64
+ // project; adding another is refused until one is removed.
65
+ maxAddedAgents: 16,
59
66
  // Files compared on disk before and after a message, to record which changed during it; past
60
67
  // this many the comparison stops and the record says it is partial. Changes kept per message;
61
68
  // more are counted.
@@ -128,7 +135,13 @@ function compare(before, after, limit) {
128
135
  changed.sort((a, b) => (a.modifiedAt ?? '\uffff').localeCompare(b.modifiedAt ?? '\uffff') || a.path.localeCompare(b.path));
129
136
  return { changed: changed.slice(0, limit), omitted: Math.max(0, changed.length - limit), partial };
130
137
  }
131
- const backends = ['claude-acp', 'codex-acp', 'acp'];
138
+ const builtIn = ['claude-acp', 'codex-acp', 'acp'];
139
+ const addedName = /^acp:[a-z0-9][a-z0-9-]{0,39}$/;
140
+ const backendName = (value) => typeof value === 'string' && (builtIn.includes(value) || addedName.test(value));
141
+ const generic = (backend) => backend === 'acp' || !!backend?.startsWith('acp:');
142
+ const sameCommand = (a, b) => a.file === b.file && a.args.length === b.args.length && a.args.every((arg, i) => arg === b.args[i]);
143
+ // DeepSeek Harness's own ACP command, which continues the sessions Harness keeps in this folder.
144
+ const harnessCommand = (command) => !!command && (knownAcpAgent(command)?.id === 'dsh' || [command.file, ...command.args].some(part => /(^|[\\/])dsh(\.[a-z]+)?$/i.test(part)));
132
145
  const blankSettings = () => ({ format: 1, version: 0, updatedAt: null, backend: null, choices: {}, offered: {} });
133
146
  const acpDriver = (driver) => !!driver && 'configure' in driver;
134
147
  const identity = /^c-[a-f0-9]{16}$/;
@@ -138,6 +151,9 @@ function cut(text, maxBytes) {
138
151
  // Cut on a character boundary; the cut is reported, never silent.
139
152
  return { text: Buffer.from(text).subarray(0, maxBytes).toString('utf8').replace(/�+$/, ''), cut: true };
140
153
  }
154
+ // Some Agents (Kimi Code) title a session with the start of its first prompt, which carries the
155
+ // interface context Rho appends to the person's words; the title keeps the person's words.
156
+ const agentTitleOf = (title) => title ? title.split(/\s*Rho interface context for this message:/)[0].trim() || null : null;
141
157
  function titleOf(text) { const line = text.trim().split('\n')[0].trim(); return line.length > 80 ? line.slice(0, 79) + '…' : line; }
142
158
  // An Agent reports a session it no longer has, in its own words: the ACP load answers "Resource
143
159
  // not found", Claude Code says the session does not exist, and a load that cannot run at all says
@@ -187,6 +203,8 @@ export class Conversations {
187
203
  drivers = new Map();
188
204
  permissions = new Map();
189
205
  checks = new Map();
206
+ // Agents being added, by command: a second request for the same command waits for the first.
207
+ adding = new Map();
190
208
  // Descriptions being written by a model now, by project, conversation, message and file.
191
209
  writing = new Set();
192
210
  describing = { ...describeLimits };
@@ -233,7 +251,7 @@ export class Conversations {
233
251
  const last = c.turns.at(-1), usage = c.observation?.usage;
234
252
  const costs = c.turns.map(t => t.result?.costUsd).filter((v) => typeof v === 'number');
235
253
  return { conversationId: c.id, changeId: c.changeId, requestDigest: c.requestDigest, title: c.title, client: c.client, principal: c.principal, createdAt: c.createdAt, updatedAt: c.updatedAt, version: c.version, turns: c.turns.length, state: last?.state ?? 'idle', lastAt: last?.endedAt ?? last?.sentAt ?? c.createdAt,
236
- listed: 2, turnLimit: !!last && agentTurnLimit(last), waitingPermission: last?.state === 'running' && !!last.permissions?.some(p => p.state === 'pending'), backend: c.backend ?? 'claude-cli', agentTitle: c.observation?.info?.title ?? null, adapter: c.adapter ?? null, model: [...c.turns].reverse().find(t => t.model)?.model ?? null, config: c.config ?? {},
254
+ listed: 2, turnLimit: !!last && agentTurnLimit(last), waitingPermission: last?.state === 'running' && !!last.permissions?.some(p => p.state === 'pending'), backend: c.backend ?? 'claude-cli', agentTitle: agentTitleOf(c.observation?.info?.title), adapter: c.adapter ?? null, model: [...c.turns].reverse().find(t => t.model)?.model ?? null, config: c.config ?? {},
237
255
  usage: usage ? { used: usage.used, size: usage.size, cost: usage.cost ?? null } : null, costUsd: costs.length ? costs.reduce((a, b) => a + b, 0) : null,
238
256
  sessionId: c.acpSessionId ?? c.nativeSessionId ?? null, trashedAt: c.trashedAt ?? null,
239
257
  origin: c.origin ? { producer: c.origin.producer, where: c.origin.where, ...(c.origin.copy ? { copyOf: c.origin.sessionId } : {}) } : null };
@@ -393,12 +411,12 @@ export class Conversations {
393
411
  // Without an Agent or settings named in the request, the person's saved ones are used and
394
412
  // recorded with the conversation.
395
413
  async start(project, by, input) {
396
- requireThat(!input.backend || backends.includes(input.backend), 'invalid', 'Choose Claude Code or Codex.');
414
+ requireThat(!input.backend || backendName(input.backend), 'invalid', 'Choose an Agent Rho starts.');
397
415
  if (input.config)
398
416
  this.choices(input.config);
399
417
  const from = input.continue;
400
418
  const text = this.message(input.text), requestDigest = digest(canonical({ text, ...(input.contexts?.length ? { contexts: input.contexts } : {}), ...(input.backend ? { backend: input.backend } : {}), ...(input.config ? { config: input.config } : {}),
401
- ...(from ? { continue: { producer: from.producer, sessionId: from.sessionId, copy: from.copy } } : {}) }));
419
+ ...(input.uiContext ? { uiContext: input.uiContext } : {}), ...(from ? { continue: { producer: from.producer, sessionId: from.sessionId, copy: from.copy } } : {}) }));
402
420
  return this.serial(project.id, async () => {
403
421
  const index = await this.index(project), existing = index.conversations.find(x => x.changeId === input.changeId);
404
422
  requireThat(!index.deleted?.some(x => x.changeId === input.changeId), 'conflict', 'That request started a conversation that was later deleted. It is not started again.');
@@ -415,9 +433,13 @@ export class Conversations {
415
433
  }
416
434
  this.capacity();
417
435
  const saved = await this.readSettings();
418
- const backend = from?.backend ?? input.backend ?? saved.backend ?? this.config.backend ?? 'claude-acp';
436
+ // A saved Agent the person has since removed is not used; the service's own is.
437
+ const savedBackend = saved.backend && (!saved.backend.startsWith('acp:') || this.commandOf(saved.backend, saved)) ? saved.backend : null;
438
+ const backend = from?.backend ?? input.backend ?? savedBackend ?? this.config.backend ?? 'claude-acp';
439
+ const command = generic(backend) ? this.commandOf(backend, saved) : undefined;
440
+ requireThat(!generic(backend) || command, 'unavailable', backend === 'acp' ? 'No ACP command was named when Rho started. Add the Agent under Agents in Rho. Nothing was sent.' : 'That Agent is not added in Rho. Add it under Agents first. Nothing was sent.');
419
441
  const config = backend === 'claude-cli' ? undefined : input.config ?? saved.choices[backend];
420
- 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 } : {}) };
442
+ 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' : generic(backend) ? 'acp' : 'claude-code', nativeSessionId: backend === 'claude-cli' ? randomUUID() : null, confirmed: false, principal: null, title: titleOf(text), turns: [], ...(config && Object.keys(config).length ? { config } : {}), ...(command ? { acpCommand: command } : {}) };
421
443
  for (const id of input.contexts ?? [])
422
444
  await this.ports.context?.(project, id);
423
445
  if (from) {
@@ -431,6 +453,8 @@ export class Conversations {
431
453
  const turn = this.turn(input.changeId, requestDigest, by, text, 1);
432
454
  if (input.contexts?.length)
433
455
  turn.contexts = [...input.contexts];
456
+ if (input.uiContext)
457
+ turn.uiContext = await this.ports.uiContext?.(project, input.uiContext, this.limits);
434
458
  c.turns.push(turn);
435
459
  // Recorded before the process starts, so a crash in between leaves an unknown message, never a resend.
436
460
  await this.write(project, c);
@@ -462,7 +486,7 @@ export class Conversations {
462
486
  requireThat(!holder, 'condition', `This session is open in ${holder?.where ?? 'another window'}. Close it there to continue here${existing ? '' : ', or continue a copy'}. Nothing was sent.`, { heldBy: holder?.where ?? null });
463
487
  }
464
488
  async send(project, by, input) {
465
- const text = this.message(input.text), requestDigest = digest(canonical({ text, ...(input.contexts?.length ? { contexts: input.contexts } : {}), ...(input.continuationOf ? { continuationOf: input.continuationOf } : {}) }));
489
+ const text = this.message(input.text), requestDigest = digest(canonical({ text, ...(input.contexts?.length ? { contexts: input.contexts } : {}), ...(input.uiContext ? { uiContext: input.uiContext } : {}), ...(input.continuationOf ? { continuationOf: input.continuationOf } : {}) }));
466
490
  return this.serial(project.id, async () => {
467
491
  const c = await this.load(project, input.conversationId), existing = c.turns.find(t => t.changeId === input.changeId);
468
492
  if (existing) {
@@ -499,6 +523,8 @@ export class Conversations {
499
523
  turn.continuationOf = input.continuationOf;
500
524
  if (input.contexts?.length)
501
525
  turn.contexts = [...input.contexts];
526
+ if (input.uiContext)
527
+ turn.uiContext = await this.ports.uiContext?.(project, input.uiContext, this.limits);
502
528
  c.turns.push(turn);
503
529
  await this.write(project, c);
504
530
  this.launch(project, c, turn);
@@ -511,7 +537,7 @@ export class Conversations {
511
537
  this.choices({ [input.configId]: input.value });
512
538
  return this.serial(project.id, async () => {
513
539
  const c = await this.load(project, input.conversationId), last = c.turns.at(-1);
514
- requireThat(c.backend === 'claude-acp' || c.backend === 'codex-acp' || c.backend === 'acp', 'unavailable', 'This conversation\'s Agent has no settings Rho can change.');
540
+ requireThat(c.backend === 'claude-acp' || c.backend === 'codex-acp' || generic(c.backend), 'unavailable', 'This conversation\'s Agent has no settings Rho can change.');
515
541
  requireThat(!c.trashedAt, 'conflict', 'This conversation is in the trash. Restore it to change its settings.');
516
542
  requireThat(!last || last.state !== 'running' || !this.liveTurn(project, c, last), 'conflict', 'The Agent is answering. Change its settings after this message.');
517
543
  const driver = this.drivers.get(project.id + '/' + c.id)?.driver;
@@ -552,32 +578,53 @@ export class Conversations {
552
578
  return value;
553
579
  }
554
580
  settingsView(s) {
555
- const service = this.config.backend ?? 'claude-acp';
556
- return { version: s.version, updatedAt: s.updatedAt, backend: s.backend, serviceBackend: service, choices: s.choices, offered: s.offered, checking: [...this.checks.keys()], describe: { enabled: s.describe?.enabled !== false }, agents: this.agents() };
581
+ const service = this.config.backend ?? 'claude-acp', added = s.added ?? [];
582
+ return { version: s.version, updatedAt: s.updatedAt, backend: s.backend, serviceBackend: service, choices: s.choices, offered: s.offered, checking: [...this.checks.keys()], describe: { enabled: s.describe?.enabled !== false }, agents: this.agents(s),
583
+ added: added.map(a => ({ backend: a.backend, name: a.name, command: [a.command.file, ...a.command.args], addedAt: a.addedAt })),
584
+ // The ACP Agents Rho knows that are installed here and not yet added. Finding them reads PATH.
585
+ installed: installedAcpAgents(process.env).filter(i => !(this.config.acpCommand && sameCommand(i.command, this.config.acpCommand)) && !added.some(a => sameCommand(a.command, i.command)))
586
+ .map(i => ({ id: i.id, name: i.name, command: [i.command.file, ...i.command.args] })) };
587
+ }
588
+ // The command that starts an ACP Agent other than Claude Code or Codex: one the person added, or
589
+ // the one named when this service started.
590
+ commandOf(backend, s) {
591
+ return backend === 'acp' ? this.config.acpCommand : s.added?.find(a => a.backend === backend)?.command;
557
592
  }
558
593
  // Which Agents this service can start: the person's installed clients, found as the adapters
559
- // find them, and an ACP command the person named. Looking starts nothing.
560
- agents() {
594
+ // find them, the ACP command named when it started and the ACP Agents the person added. Looking
595
+ // starts nothing. The named command's Agent is named as Rho knows it, else as it named itself the
596
+ // last time it answered, else by its program.
597
+ agents(s) {
561
598
  const found = (problem) => problem ? { found: false, problem } : { found: true };
562
- const command = this.config.acpCommand;
599
+ const command = this.config.acpCommand, reported = s.offered.acp?.adapter?.name;
563
600
  return {
564
601
  // A command the embedding names is its own choice and is not looked for, as the adapters do.
565
602
  'claude-acp': found(this.config.claudeAcp?.command ? null : missingClient('claude', acpEnvironment('claude', { ...process.env }, this.binary))),
566
603
  'codex-acp': found(this.config.codexAcp?.command ? null : missingClient('codex', acpEnvironment('codex', { ...process.env }, this.config.codex))),
567
- acp: command ? { found: true, name: /(^|[\\/])dsh(\.[a-z]+)?$/i.test(command.file) || command.args.some(a => /(^|[\\/])dsh(\.[a-z]+)?$/i.test(a)) ? 'DeepSeek Harness' : null } : { found: false, problem: 'Start Rho with --agent-backend acp --acp-command to use another ACP Agent.' }
604
+ acp: command ? { found: true, name: knownAcpAgent(command)?.name ?? (harnessCommand(command) ? 'DeepSeek Harness' : reported || path.basename(command.file).replace(/\.(exe|cmd)$/i, '')) } : { found: false, problem: 'No ACP command was named when Rho started.' },
605
+ ...Object.fromEntries((s.added ?? []).map(a => [a.backend, locate(a.command.file, process.env) ? { found: true, name: a.name }
606
+ : { found: false, name: a.name, problem: `${a.name} is no longer at ${a.command.file}. Install it again, or remove it and add it again.` }]))
568
607
  };
569
608
  }
609
+ // The Agent that continues DeepSeek Harness's sessions: Harness's own ACP command, named when
610
+ // Rho started or added by the person.
611
+ async harness() {
612
+ if (harnessCommand(this.config.acpCommand))
613
+ return 'acp';
614
+ return (await this.readSettings()).added?.find(a => harnessCommand(a.command))?.backend ?? null;
615
+ }
570
616
  async settings() { return this.settingsView(await this.readSettings()); }
571
617
  // The person's Agent and settings for new conversations. Each Agent's choices are replaced as a
572
618
  // whole and must be among the values it last offered; a stale version is refused.
573
619
  async updateSettings(input) {
574
620
  requireThat(this.config.settingsFile, 'unavailable', 'This service keeps no Agent settings.');
575
- requireThat(input.backend == null || backends.includes(input.backend), 'invalid', 'Choose Claude Code or Codex.');
576
621
  return this.serial('settings', async () => {
577
622
  const s = await this.readSettings();
578
623
  requireThat(s.version === input.expectedVersion, 'conflict', 'The Agent settings changed in another tab. Read them again.', { version: s.version });
624
+ const startable = (backend) => backendName(backend) && (builtIn.includes(backend) || !!this.commandOf(backend, s));
625
+ requireThat(input.backend == null || startable(input.backend), 'invalid', 'Choose an Agent Rho starts.');
579
626
  for (const [backend, values] of Object.entries(input.choices ?? {})) {
580
- requireThat(backends.includes(backend), 'invalid', 'Choose Claude Code or Codex.');
627
+ requireThat(startable(backend), 'invalid', 'Choose an Agent Rho starts.');
581
628
  this.choices(values);
582
629
  const offered = s.offered[backend]?.options;
583
630
  requireThat(offered || !Object.keys(values).length, 'unavailable', 'Check what this Agent offers first.');
@@ -601,7 +648,7 @@ export class Conversations {
601
648
  // ends the process; no message is sent. The protected resource is local Agent processes: one
602
649
  // check per Agent at a time (a second request waits for the same one), each bounded by connectTimeoutMs.
603
650
  async check(project, input) {
604
- requireThat(backends.includes(input.backend), 'invalid', 'Choose Claude Code or Codex.');
651
+ requireThat(backendName(input.backend), 'invalid', 'Choose an Agent Rho starts.');
605
652
  requireThat(this.config.settingsFile, 'unavailable', 'This service keeps no Agent settings.');
606
653
  requireThat(!this.closing, 'unavailable', 'Rho is closing. Nothing was started.');
607
654
  let pending = this.checks.get(input.backend);
@@ -615,7 +662,9 @@ export class Conversations {
615
662
  async inspect(project, backend) {
616
663
  const { ClaudeAcpConversation, CodexAcpConversation, GenericAcpConversation } = await import('@rho-workspace/plugin-agent/conversation');
617
664
  const common = { cwd: project.root, mcpServers: [], limits: this.limits };
618
- 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 });
665
+ const command = generic(backend) ? this.commandOf(backend, await this.readSettings()) : undefined;
666
+ requireThat(!generic(backend) || command, 'unavailable', backend === 'acp' ? 'No ACP command was named when Rho started.' : 'That Agent is not added in Rho.');
667
+ const driver = backend === 'codex-acp' ? new CodexAcpConversation({ ...this.config.codexAcp, ...common, codex: this.config.codex }) : command ? new GenericAcpConversation({ ...common, command }) : new ClaudeAcpConversation({ preferred: claudePreferred, ...this.config.claudeAcp, ...common, claude: this.binary });
619
668
  let found;
620
669
  try {
621
670
  found = await driver.inspect();
@@ -636,6 +685,77 @@ export class Conversations {
636
685
  await atomicJson(this.config.settingsFile, s);
637
686
  });
638
687
  }
688
+ // An ACP Agent the person adds for new conversations in every project. Rho starts its command once
689
+ // in this project, as a check does (it opens a session, reads what the Agent offers and removes
690
+ // the session; no message is sent), and keeps it only when the Agent answered. The command is
691
+ // the person's own: a program on PATH or a path to one, and its arguments. Adding the same
692
+ // command again checks it again; one start per command runs at a time.
693
+ async addAgent(project, input) {
694
+ requireThat(this.config.settingsFile, 'unavailable', 'This service keeps no Agent settings.');
695
+ requireThat(!this.closing, 'unavailable', 'Rho is closing. Nothing was started.');
696
+ const [program, ...args] = input.command;
697
+ const file = locate(program, process.env);
698
+ requireThat(file, 'unavailable', `${program} was not found ${/[\\/]/.test(program) ? 'there' : 'on PATH'}. Install it, or give the path of its program.`);
699
+ const command = { file: path.resolve(file), args }, key = JSON.stringify(command);
700
+ let pending = this.adding.get(key);
701
+ if (!pending) {
702
+ pending = this.addOnce(project, command).finally(() => this.adding.delete(key));
703
+ this.adding.set(key, pending);
704
+ }
705
+ return pending;
706
+ }
707
+ async addOnce(project, command) {
708
+ const known = knownAcpAgent(command), label = known?.name ?? path.basename(command.file);
709
+ const room = (s) => requireThat(s.added?.some(a => sameCommand(a.command, command)) || (s.added?.length ?? 0) < this.limits.maxAddedAgents, 'limit', `Rho keeps at most ${this.limits.maxAddedAgents} added Agents. Remove one first. Nothing was started.`, { limit: 'maxAddedAgents', maximum: this.limits.maxAddedAgents });
710
+ room(await this.readSettings());
711
+ const { GenericAcpConversation } = await import('@rho-workspace/plugin-agent/conversation');
712
+ let found;
713
+ try {
714
+ found = await new GenericAcpConversation({ cwd: project.root, mcpServers: [], limits: this.limits, command }).inspect();
715
+ }
716
+ catch (error) {
717
+ throw new Fault('unavailable', `${label} did not start as an ACP Agent (${(error instanceof Error ? error.message : String(error)).replace(/\.$/, '')}). Nothing was added.`);
718
+ }
719
+ return this.serial('settings', async () => {
720
+ const s = await this.readSettings(), added = s.added ?? [], existing = added.find(a => sameCommand(a.command, command));
721
+ room(s);
722
+ const name = existing?.name ?? known?.name ?? found.adapter?.name ?? label;
723
+ const backend = existing?.backend ?? this.newName(known?.id ?? found.adapter?.name ?? label, added);
724
+ if (!existing)
725
+ s.added = [...added, { backend, name, command, addedAt: now() }];
726
+ s.offered[backend] = { at: now(), source: 'check', adapter: found.adapter, options: found.options, omitted: found.omitted };
727
+ s.version++;
728
+ s.updatedAt = now();
729
+ await atomicJson(this.config.settingsFile, s);
730
+ return this.settingsView(s);
731
+ });
732
+ }
733
+ newName(seed, added) {
734
+ const base = seed.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 32) || 'agent';
735
+ for (let n = 1;; n++) {
736
+ const name = `acp:${base}${n > 1 ? '-' + n : ''}`;
737
+ if (!added.some(a => a.backend === name))
738
+ return name;
739
+ }
740
+ }
741
+ // Removing an added Agent takes it out of every Agent menu. Its conversations stay, each with
742
+ // the command it started with, so they can still be read and continued.
743
+ async removeAgent(input) {
744
+ requireThat(this.config.settingsFile, 'unavailable', 'This service keeps no Agent settings.');
745
+ return this.serial('settings', async () => {
746
+ const s = await this.readSettings(), backend = input.backend;
747
+ requireThat(s.added?.some(a => a.backend === backend), 'not_found', 'No Agent added in Rho has that name.');
748
+ s.added = s.added.filter(a => a.backend !== backend);
749
+ delete s.choices[backend];
750
+ delete s.offered[backend];
751
+ if (s.backend === backend)
752
+ s.backend = null;
753
+ s.version++;
754
+ s.updatedAt = now();
755
+ await atomicJson(this.config.settingsFile, s);
756
+ return this.settingsView(s);
757
+ });
758
+ }
639
759
  // A stop is a request until the driver confirms it; what was already done stays done.
640
760
  async stop(project, by, input) {
641
761
  return this.serial(project.id, async () => {
@@ -817,7 +937,7 @@ export class Conversations {
817
937
  const observed = { observation: c.observation, observe: (observation) => {
818
938
  requireThat(observation.sessionId === c.acpSessionId, 'conflict', 'The observed Agent session differs from the retained identity.');
819
939
  requireThat(Buffer.byteLength(JSON.stringify(observation)) <= this.limits.maxObservationBytes, 'limit', 'The session projection exceeded maxObservationBytes; prior observations were kept.');
820
- 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');
940
+ 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' || generic(c.backend));
821
941
  c.observation = observation;
822
942
  this.schedule(project, c);
823
943
  for (const live of this.live.values())
@@ -834,12 +954,15 @@ export class Conversations {
834
954
  // Every session started here is told about the project's shared notes and its section.
835
955
  const instructions = notesInstructions(c.id, { notebook: !!this.ports.notebook?.() });
836
956
  const common = { cwd: project.root, mcpServers, limits: this.limits, sessionId: c.acpSessionId, ...(!c.acpSessionId && c.forkFrom ? { forkFrom: c.forkFrom } : {}), config: c.config, instructions, ...observed };
957
+ // A conversation started before Rho kept its ACP command uses the one named at this start.
958
+ const command = generic(c.backend) ? c.acpCommand ?? (c.backend === 'acp' ? this.config.acpCommand : undefined) : undefined;
959
+ requireThat(!generic(c.backend) || command, 'unavailable', 'This conversation\'s ACP Agent was named when Rho started, and this start names none. Start Rho with the same --acp-command to continue it.');
837
960
  const driver = c.backend === 'codex-acp'
838
961
  ? new CodexAcpConversation({ ...this.config.codexAcp, ...common, codex: this.config.codex })
839
962
  : c.backend === 'claude-acp'
840
963
  ? new ClaudeAcpConversation({ preferred: claudePreferred, ...this.config.claudeAcp, ...common, claude: this.binary, plugins: [...this.config.claudeAcp?.plugins ?? [], ...await this.ports.claudePlugins?.(project) ?? []] })
841
- : c.backend === 'acp'
842
- ? new GenericAcpConversation({ ...common, command: this.config.acpCommand })
964
+ : command
965
+ ? new GenericAcpConversation({ ...common, command })
843
966
  : 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 });
844
967
  this.drivers.set(key, { driver, project, conversation: c });
845
968
  return driver;
@@ -1028,7 +1151,7 @@ export class Conversations {
1028
1151
  }
1029
1152
  }
1030
1153
  });
1031
- const utterance = turn.text + (turn.continuationOf ? '\n\nOriginal task:\n' + c.turns[0].text : '') + (turn.contexts?.length ? '\n\nRead these exact frozen review contexts with context.read before responding: ' + turn.contexts.join(', ') + '. Reply with annotation.reply and explicit result resource versions.' : '');
1154
+ const utterance = turn.text + uiContextPrompt(turn.uiContext) + (turn.continuationOf ? '\n\nOriginal task:\n' + c.turns[0].text : '') + (turn.contexts?.length ? '\n\nRead these exact frozen review contexts with context.read before responding: ' + turn.contexts.join(', ') + '. Reply with annotation.reply and explicit result resource versions.' : '');
1032
1155
  // The Agent's session ended with its process: after Rho restarts, and on a machine that
1033
1156
  // never ran the session a project was copied from. The messages Rho kept are the record, so
1034
1157
  // the message runs in a new session told what was said before. Nothing is resent as a new
@@ -1134,7 +1257,7 @@ export class Conversations {
1134
1257
  const given = shown.get(file);
1135
1258
  const reason = !enabled ? 'Descriptions by a model are switched off; the facts are kept.'
1136
1259
  : c.backend === 'codex-acp' ? 'Descriptions are written with Claude Code; this session\'s Agent is Codex.'
1137
- : c.backend === 'acp' ? 'Descriptions are written with Claude Code; this session\'s Agent is a configured ACP agent.'
1260
+ : generic(c.backend) ? 'Descriptions are written with Claude Code; this session\'s Agent is another ACP Agent.'
1138
1261
  : !this.ports.describe ? 'No model is set up to describe files.' : null;
1139
1262
  const keep = (why) => { if (record.reason !== why) {
1140
1263
  record.state = record.sentence ? 'described' : 'facts';
package/dist/discovery.js CHANGED
@@ -5,9 +5,9 @@ import { schemaClosure } from './protocol-discovery.js';
5
5
  const size = (v) => Buffer.byteLength(JSON.stringify(v)) + 64;
6
6
  export const applicationEffects = Object.fromEntries([
7
7
  ...['session.instructions', 'extension.list', 'extension.describe', 'extension.content', 'extension.read', 'protocol.describe', 'protocol.validate', 'project.entries', 'capability.list', 'skill.list', 'skill.read', 'action.lookup', 'action.observe', 'action.output', 'action.list', 'composition.read', 'observation.snapshot', 'interaction.selection.read', 'workspace.observe', 'task.map.read'].map(m => [m, 'read']),
8
- ...['composition.put', 'view.close', 'capability.configure', 'composition.apply', 'composition.undo', 'view.protection.set', 'interaction.selection.set', 'selection.set', 'workspace.enhance', 'task.map.declare', 'task.map.present', 'task.map.respond'].map(m => [m, 'metadata']),
8
+ ...['composition.put', 'view.close', 'capability.configure', 'composition.apply', 'composition.undo', 'composition.delete', 'view.protection.set', 'interaction.selection.set', 'selection.set', 'workspace.enhance', 'task.map.declare', 'task.map.present', 'task.map.respond'].map(m => [m, 'metadata']),
9
9
  ...['workspace.save', 'workspace.export', 'workspace.restore', 'action.export'].map(m => [m, 'file']),
10
- ...['extension.call', 'extension.install', 'extension.manage', 'action.retry', 'action.cancel'].map(m => [m, 'native']),
10
+ ...['extension.call', 'extension.install', 'extension.manage', 'extension.market.change', 'action.retry', 'action.cancel'].map(m => [m, 'native']),
11
11
  ]);
12
12
  const rhoSummary = 'The project in Rho: its folder\'s entries, managed runs, the views on the person\'s screen and what is selected in them, the task map, and which plugins are on.';
13
13
  const metadata = new Set(['capability.index', 'capability.resolve', 'binding.activity.append', 'task.routing.put', 'feedback.submit', 'view.observe', 'view.observations.read']);
@@ -35,6 +35,18 @@ export declare const extensionOperations: {
35
35
  required: string[];
36
36
  additionalProperties: boolean;
37
37
  };
38
+ 'extension.market.change': {
39
+ oneOf: {
40
+ type: string;
41
+ properties: Record<string, unknown>;
42
+ required: string[];
43
+ additionalProperties: boolean;
44
+ }[];
45
+ type: string;
46
+ properties: Record<string, unknown>;
47
+ required: string[];
48
+ additionalProperties: boolean;
49
+ };
38
50
  'extension.manage': {
39
51
  type: string;
40
52
  properties: Record<string, unknown>;
@@ -49,6 +61,12 @@ export declare const extensionResponses: {
49
61
  required: string[];
50
62
  additionalProperties: boolean;
51
63
  };
64
+ 'extension.market.change': {
65
+ type: string;
66
+ properties: Record<string, unknown>;
67
+ required: string[];
68
+ additionalProperties: boolean;
69
+ };
52
70
  'extension.describe': {
53
71
  type: string;
54
72
  properties: Record<string, unknown>;
@@ -88,6 +106,7 @@ export declare const extensionResponses: {
88
106
  };
89
107
  export declare const extensionDescriptions: {
90
108
  'extension.list': string;
109
+ 'extension.market.change': string;
91
110
  'extension.describe': string;
92
111
  'extension.content': string;
93
112
  'extension.call': string;
@@ -1,5 +1,6 @@
1
1
  // Installed third-party packages are application assembly, independent of the
2
2
  // functional owners compiled into Rho. This protocol never imports their code.
3
+ import { marketEntrySchema } from './extension-markets.js';
3
4
  const str = { type: 'string', minLength: 1, maxLength: 1024 };
4
5
  const id = { type: 'string', pattern: '^[a-z0-9][a-z0-9._-]{0,95}$' };
5
6
  const sha = { type: 'string', pattern: '^[a-f0-9]{64}$' };
@@ -7,13 +8,19 @@ const object = (properties, required = Object.keys(properties)) => ({ type: 'obj
7
8
  const json = { type: 'object', additionalProperties: true };
8
9
  const project = { projectId: str };
9
10
  const selected = { ...project, pluginId: id, expectedDigest: sha };
11
+ const marketOrigin = object({ id, title: str, url: str, digest: sha, keywords: { type: 'array', maxItems: 16, items: { type: 'string', minLength: 1, maxLength: 64 } } }, ['id', 'title', 'url', 'digest']);
12
+ const marketSelection = object({ id, expectedDigest: sha, pluginId: id });
13
+ const revision = { type: 'integer', minimum: 1 };
14
+ const marketChange = { ...object({ action: { enum: ['add', 'refresh', 'remove'] }, url: str, id, expectedRevision: revision }, ['action']), oneOf: [object({ action: { const: 'add' }, url: str }), object({ action: { enum: ['refresh', 'remove'] }, id, expectedRevision: revision })] };
15
+ const market = object({ id, url: str, title: str, revision, digest: sha, fetchedAt: str, checkedAt: str, error: { type: ['string', 'null'] }, plugins: { type: 'array', items: marketEntrySchema } });
10
16
  export const extensionOperations = {
11
17
  'extension.list': object(project, []),
12
18
  'extension.describe': object({ ...project, pluginId: id }),
13
19
  'extension.content': object({ ...selected, kind: { enum: ['skill', 'view'] }, entryId: id }),
14
20
  'extension.call': object({ ...selected, operation: id, input: json, changeId: { type: 'string', pattern: '^[A-Za-z0-9._:@-]{1,128}$' } }),
15
21
  'extension.read': object({ ...project, invocationId: sha }),
16
- 'extension.install': object({ source: str, replace: id, expectedDigest: sha }, ['source']),
22
+ 'extension.install': object({ source: str, replace: id, expectedDigest: sha, market: marketSelection }, ['source']),
23
+ 'extension.market.change': marketChange,
17
24
  'extension.manage': object({ pluginId: id, expectedDigest: sha, action: { enum: ['enable', 'disable', 'remove'] } }),
18
25
  };
19
26
  const array = (items) => ({ type: 'array', items });
@@ -23,15 +30,16 @@ const manifest = { ...object({ apiVersion: { const: 1 }, id, title: text, descri
23
30
  operations: array({ ...object({ id, title: text, description: text, effect: { enum: ['read', 'native'] }, methods: array(text), inputSchema: json }), additionalProperties: true }),
24
31
  skills: array(entry), views: array(entry),
25
32
  }, ['apiVersion', 'id', 'title', 'description', 'operations', 'skills', 'views']), additionalProperties: true };
26
- const installed = { id, name: text, version: text, title: text, description: text, digest: sha, source: text, sourceCommit: text, enabled: { type: 'boolean' }, installedAt: text };
27
- const installedRequired = Object.keys(installed).filter(key => key !== 'sourceCommit');
33
+ const installed = { id, name: text, version: text, title: text, description: text, digest: sha, source: text, sourceCommit: text, market: marketOrigin, enabled: { type: 'boolean' }, installedAt: text };
34
+ const installedRequired = Object.keys(installed).filter(key => !['sourceCommit', 'market'].includes(key));
28
35
  const descriptor = object({ ...installed, manifest }, [...installedRequired, 'manifest']);
29
36
  const receipt = object({ invocationId: sha, caller: text, projectId: text, pluginId: id, packageDigest: sha, operation: id, requestDigest: sha,
30
37
  state: { enum: ['pending', 'completed', 'unknown'] }, startedAt: text, endedAt: text, value: {}, error: text,
31
38
  delegations: array(object({ method: text, request: {}, state: { enum: ['pending', 'completed', 'unknown'] }, value: {}, error: text }, ['method', 'request', 'state'])),
32
39
  }, ['invocationId', 'caller', 'projectId', 'pluginId', 'packageDigest', 'operation', 'requestDigest', 'state', 'startedAt']);
33
40
  export const extensionResponses = {
34
- 'extension.list': object({ plugins: array(object(installed, installedRequired)), directory: array(object({ id, title: text, version: text, description: text, source: text, repository: text, needs: text })), limits: json }),
41
+ 'extension.list': object({ plugins: array(object(installed, installedRequired)), directory: array(object({ id, title: text, version: text, description: text, source: text, repository: text, needs: text })), markets: array(market), limits: json }),
42
+ 'extension.market.change': object({ markets: array(market) }),
35
43
  'extension.describe': descriptor,
36
44
  'extension.install': descriptor,
37
45
  'extension.content': object({ pluginId: id, packageDigest: sha, entryId: id, kind: { enum: ['skill', 'view'] }, title: text, text }),
@@ -40,7 +48,8 @@ export const extensionResponses = {
40
48
  'extension.manage': object({ pluginId: id, action: { enum: ['enable', 'disable', 'remove'] }, retained: text }),
41
49
  };
42
50
  export const extensionDescriptions = {
43
- 'extension.list': 'List independently installed third-party packages and their enabled state, source and immutable digest. Reads start no plugin or runtime. Use extension.describe to discover operation schemas and Skills.',
51
+ 'extension.list': 'List installed third-party packages, the bundled directory and retained remote market indexes. Listing makes no network request and starts no plugin or runtime. Use extension.describe for installed operation schemas and Skills.',
52
+ 'extension.market.change': 'Local person/operator: add an HTTPS plugin market index, refresh it explicitly, or remove its source. Failed refresh retains the last valid index; removal preserves installed plugins. Listing never refreshes a source.',
44
53
  'extension.describe': 'Read an installed third-party package manifest, operation input schemas, required Rho methods, Skills and views. Read its Skill with extension.content before use. Disabled packages cannot run.',
45
54
  'extension.content': 'Read one installed plugin Skill or standalone HTML view at its selected package digest. Reading starts no plugin or runtime.',
46
55
  'extension.call': 'Call an enabled third-party operation at the described package digest, under the caller and project. The package runs trusted native code. Non-read calls retain the original changeId: repeat observes that invocation, including unknown outcomes, without re-execution. Completion is not scientific validity.',
@@ -48,5 +57,5 @@ export const extensionDescriptions = {
48
57
  'extension.install': 'Local person/operator: install a trusted prebuilt npm package, local folder, tarball, or Git source. Lifecycle scripts are disabled. Replacement stages and checks the new package before switching; use replace and expectedDigest for updates.',
49
58
  'extension.manage': 'Local person/operator: enable, disable or remove an installed third-party package at the expected digest. Existing native tasks, R sessions, project files and retained receipts are preserved.',
50
59
  };
51
- export const extensionManagement = ['extension.install', 'extension.manage'];
60
+ export const extensionManagement = ['extension.install', 'extension.manage', 'extension.market.change'];
52
61
  export const extensionMethods = Object.keys(extensionOperations);
@@ -0,0 +1,74 @@
1
+ export declare const marketLimits: {
2
+ maxMarkets: number;
3
+ maxMarketBytes: number;
4
+ maxMarketEntries: number;
5
+ marketTimeoutMs: number;
6
+ };
7
+ export declare const marketEntrySchema: {
8
+ type: string;
9
+ properties: Record<string, unknown>;
10
+ required: string[];
11
+ additionalProperties: boolean;
12
+ };
13
+ export declare const marketIndexSchema: {
14
+ type: string;
15
+ properties: Record<string, unknown>;
16
+ required: string[];
17
+ additionalProperties: boolean;
18
+ };
19
+ export interface MarketEntry {
20
+ id: string;
21
+ title: string;
22
+ version: string;
23
+ description: string;
24
+ source: string;
25
+ repository: string;
26
+ needs: string;
27
+ keywords?: string[];
28
+ }
29
+ export interface ExtensionMarket {
30
+ id: string;
31
+ url: string;
32
+ title: string;
33
+ revision: number;
34
+ digest: string;
35
+ fetchedAt: string;
36
+ checkedAt: string;
37
+ error: string | null;
38
+ plugins: MarketEntry[];
39
+ }
40
+ export interface MarketSelection {
41
+ id: string;
42
+ expectedDigest: string;
43
+ pluginId: string;
44
+ }
45
+ export type MarketChange = {
46
+ action: 'add';
47
+ url: string;
48
+ } | {
49
+ action: 'refresh' | 'remove';
50
+ id: string;
51
+ expectedRevision: number;
52
+ };
53
+ export declare class ExtensionMarkets {
54
+ readonly directory: string;
55
+ readonly limits: typeof marketLimits;
56
+ private values;
57
+ constructor(directory: string, limits: typeof marketLimits);
58
+ init(): Promise<void>;
59
+ list(): ExtensionMarket[];
60
+ private save;
61
+ private fetch;
62
+ private observe;
63
+ change(input: MarketChange): Promise<ExtensionMarket[]>;
64
+ select(selection: MarketSelection, source: string): {
65
+ entry: MarketEntry;
66
+ origin: {
67
+ id: string;
68
+ title: string;
69
+ url: string;
70
+ digest: string;
71
+ keywords: string[];
72
+ };
73
+ };
74
+ }