@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.
- package/dist/agent-connect.d.ts +36 -2
- package/dist/agent-connect.js +64 -11
- package/dist/answer-board.js +3 -3
- package/dist/bridge.js +6 -1
- package/dist/browser-access.d.ts +1 -0
- package/dist/browser-access.js +5 -3
- package/dist/capabilities.d.ts +21 -5
- package/dist/capabilities.js +6 -4
- package/dist/cli.js +76 -11
- package/dist/composition.d.ts +1 -1
- package/dist/composition.js +13 -3
- package/dist/connect-cli.d.ts +50 -0
- package/dist/connect-cli.js +128 -0
- package/dist/contract.generated.d.ts +1191 -278
- package/dist/contract.generated.js +1 -1
- package/dist/contracts/owners.generated.js +845 -47
- package/dist/conversation-presentation.d.ts +9 -2
- package/dist/conversation-presentation.js +3 -1
- package/dist/conversations.d.ts +154 -8
- package/dist/conversations.js +147 -24
- package/dist/discovery.js +2 -2
- package/dist/extension-contract.d.ts +19 -0
- package/dist/extension-contract.js +15 -6
- package/dist/extension-markets.d.ts +74 -0
- package/dist/extension-markets.js +166 -0
- package/dist/extensions.d.ts +29 -1
- package/dist/extensions.js +13 -4
- package/dist/external-work.d.ts +22 -0
- package/dist/external-work.js +91 -0
- package/dist/plugin-cli.js +75 -3
- package/dist/plugin-inventory.d.ts +56 -0
- package/dist/plugin-inventory.js +41 -0
- package/dist/plugin-inventory.json +1581 -102
- package/dist/plugin-presentation.d.ts +105 -2
- package/dist/plugin-presentation.js +59 -5
- package/dist/plugins.d.ts +14 -3
- package/dist/plugins.js +12 -6
- package/dist/project-notes.js +1 -1
- package/dist/protocol.d.ts +6 -0
- package/dist/protocol.js +30 -12
- package/dist/replay.js +4 -0
- package/dist/response-contracts.js +26 -11
- package/dist/running.d.ts +10 -0
- package/dist/running.js +45 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +71 -8
- package/dist/state.d.ts +8 -0
- package/dist/state.js +8 -0
- package/dist/studio-layout.js +3 -3
- package/dist/task-frame.d.ts +1 -1
- package/dist/timeline-events.d.ts +1 -0
- package/dist/timeline-events.js +8 -1
- package/dist/ui-context.d.ts +73 -0
- package/dist/ui-context.js +98 -0
- package/dist/ui-contract.d.ts +23 -0
- package/dist/web/.vite/manifest.json +133 -74
- package/dist/web/assets/answer-board-CeYS8k8y.js +1 -0
- package/dist/web/assets/choices-D8vGwsl2.js +1 -0
- package/dist/web/assets/{console-Trqc0swD.js → console-Dyuo1q4N.js} +4 -4
- package/dist/web/assets/{dist-pF902jHA.js → dist-CSiCyBml.js} +1 -1
- package/dist/web/assets/{dist-DpIHRk3B.js → dist-D9FLb9Hm.js} +1 -1
- package/dist/web/assets/{dist-eMx6b8N1.js → dist-FXbTBwML.js} +1 -1
- package/dist/web/assets/{dist-DZmszrjG.js → dist-l3DUhbw5.js} +1 -1
- package/dist/web/assets/{explore-in-BCHqjJ7B.js → explore-in-BfSFGRh2.js} +1 -1
- package/dist/web/assets/graph-BU2_o5uA.js +2 -0
- package/dist/web/assets/index-C16mXVvk.css +1 -0
- package/dist/web/assets/index-CWg9XYXx.js +952 -0
- package/dist/web/assets/markdown.reader-Ct2-e2Zt.png +0 -0
- package/dist/web/assets/{notes-meaning-DcVGUBBY.js → notes-meaning-T2m5zQfv.js} +1 -1
- package/dist/web/assets/overview-PzfLJCo-.js +6 -0
- package/dist/web/assets/preview-BguEdsza.js +1 -0
- package/dist/web/assets/previews-pWNik-2g.js +3 -0
- package/dist/web/assets/{review-DYP0aCXb.js → review-Dy_85lcH.js} +1 -1
- package/dist/web/assets/rho.editor-CCJeyHma.png +0 -0
- package/dist/web/assets/rho.flow-WkMj6fRS.png +0 -0
- package/dist/web/assets/rho.media-Cc6amodO.png +0 -0
- package/dist/web/assets/rho.notebook-6vsZKGDq.png +0 -0
- package/dist/web/assets/rho.object-BGoeXjte.png +0 -0
- package/dist/web/assets/rho.plots-yIiISCkb.png +0 -0
- package/dist/web/assets/rho.scatter-DwHxesCs.png +0 -0
- package/dist/web/assets/rho.servers-DKf_mnNq.png +0 -0
- package/dist/web/assets/rho.table-DWOGaWDa.png +0 -0
- package/dist/web/assets/rho.variables-CKcWczhQ.png +0 -0
- package/dist/web/assets/{skills-1ec0Z-jx.js → skills-Dyz7mBlc.js} +1 -1
- package/dist/web/assets/{skills-work-DNexV4OJ.js → skills-work-BEGqlCkx.js} +1 -1
- package/dist/web/assets/studio-process-D-ykbC4D.js +2 -0
- package/dist/web/assets/timeline-events-DKTho9fU.js +1 -0
- package/dist/web/assets/work-turns-DSabCogR.js +1 -0
- package/dist/web/assets/{workspace-COmCbGV3.js → workspace-DfjBL--r.js} +2 -2
- package/dist/web/composition.json +138 -83
- package/dist/web/index.html +3 -3
- package/dist/web/renderer.json +1 -1
- package/dist/web-assets.js +2 -2
- package/dist/work-graph.d.ts +11 -1
- package/dist/work-graph.js +16 -5
- package/dist/workspace.d.ts +2 -2
- package/dist/workspace.js +8 -7
- package/docs/FRONTEND.md +53 -11
- package/docs/HEADLESS.md +104 -11
- package/docs/THIRD-PARTY-PLUGINS.md +75 -3
- package/node_modules/@rho-workspace/plugin-agent/README.md +5 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +31 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +45 -10
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +19 -3
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +3 -2
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +1 -1
- package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +8 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/index.js +3 -2
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.d.ts +13 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.js +13 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp.js +16 -8
- package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +1 -1
- package/node_modules/@rho-workspace/plugin-agent/package.json +1 -1
- package/node_modules/@rho-workspace/plugin-figures/LICENSE +661 -0
- package/node_modules/@rho-workspace/plugin-figures/README.md +191 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/index.d.ts +236 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/index.js +165 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/operations.d.ts +44 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/operations.js +39 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/presentation.d.ts +19 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/presentation.js +26 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/record.d.ts +188 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/record.js +143 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.js +68 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui.d.ts +56 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui.js +363 -0
- package/node_modules/@rho-workspace/plugin-figures/package.json +41 -0
- package/node_modules/@rho-workspace/plugin-figures/py/figures.py +856 -0
- package/node_modules/@rho-workspace/plugin-figures/r/figures.R +775 -0
- package/node_modules/@rho-workspace/plugin-kernel/README.md +27 -3
- package/node_modules/@rho-workspace/plugin-kernel/dist/contract.d.ts +12 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/contract.js +8 -1
- package/node_modules/@rho-workspace/plugin-kernel/dist/index.d.ts +61 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/index.js +125 -9
- package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.d.ts +14 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.js +9 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-base.d.ts +23 -5
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook-work.js +1 -1
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.d.ts +6 -4
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.js +44 -17
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-styles.js +6 -0
- package/node_modules/@rho-workspace/plugin-kernel/package.json +1 -1
- package/node_modules/@rho-workspace/plugin-kernel/runners/runner.R +144 -4
- package/node_modules/@rho-workspace/plugin-kernel/runners/runner.py +106 -1
- package/node_modules/@rho-workspace/plugin-tables/README.md +5 -0
- package/node_modules/@rho-workspace/plugin-tables/dist/index.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-tables/dist/index.js +19 -3
- package/node_modules/@rho-workspace/plugin-tables/dist/ui.js +1 -1
- package/node_modules/@rho-workspace/plugin-tables/package.json +1 -1
- package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +3 -2
- package/node_modules/jose/dist/webapi/jwks/remote.js +1 -1
- package/node_modules/jose/dist/webapi/jws/general/verify.js +2 -8
- package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +11 -8
- package/node_modules/jose/dist/webapi/lib/key_management.js +0 -1
- package/node_modules/jose/package.json +1 -1
- package/package.json +6 -4
- package/protocol/index.json +91 -55
- package/protocol/operations/agent.sessions.list.json +13 -5
- package/protocol/operations/agent.sessions.read.json +13 -5
- package/protocol/operations/capability.list.json +382 -6
- package/protocol/operations/composition.apply.json +10 -2
- package/protocol/operations/composition.delete.json +845 -0
- package/protocol/operations/composition.read.json +10 -2
- package/protocol/operations/composition.undo.json +10 -2
- package/protocol/operations/conversation.agents.add.json +346 -0
- package/protocol/operations/conversation.agents.remove.json +340 -0
- package/protocol/operations/conversation.decide.json +196 -0
- package/protocol/operations/conversation.permission.json +196 -0
- package/protocol/operations/conversation.read.json +196 -0
- package/protocol/operations/conversation.send.json +265 -0
- package/protocol/operations/conversation.settings.check.json +213 -495
- package/protocol/operations/conversation.settings.read.json +211 -490
- package/protocol/operations/conversation.settings.update.json +235 -554
- package/protocol/operations/conversation.start.json +267 -5
- package/protocol/operations/conversation.stop.json +196 -0
- package/protocol/operations/extension.describe.json +39 -0
- package/protocol/operations/extension.install.json +62 -0
- package/protocol/operations/extension.list.json +164 -1
- package/protocol/operations/extension.market.change.json +210 -0
- package/protocol/operations/figure.read.json +265 -0
- package/protocol/operations/figure.select.json +157 -0
- package/protocol/operations/kernel.interrupt.json +119 -0
- package/protocol/operations/kernel.read.json +119 -0
- package/protocol/operations/kernel.run.json +119 -0
- package/protocol/operations/observation.snapshot.json +416 -8
- package/protocol/operations/project.openFile.json +10 -2
- package/protocol/operations/session.grant.json +4 -0
- package/protocol/operations/shared.json +1 -0
- package/protocol/operations/view.close.json +10 -2
- package/protocol/operations/view.protection.set.json +10 -2
- package/vendor/composition.json +177 -112
- package/vendor/rho-workspace-plugin-agent-0.4.33.tgz +0 -0
- package/vendor/rho-workspace-plugin-figures-0.5.1.tgz +0 -0
- package/vendor/rho-workspace-plugin-kernel-0.5.13.tgz +0 -0
- package/vendor/rho-workspace-plugin-tables-0.5.2.tgz +0 -0
- package/web/theme.css +5 -0
- package/dist/web/assets/answer-board-CBkGbSLD.js +0 -1
- package/dist/web/assets/canvas-D8rDdfDz.js +0 -1
- package/dist/web/assets/choices-D-YfnFP6.js +0 -1
- package/dist/web/assets/graph-lENgfQNH.js +0 -2
- package/dist/web/assets/index-B3_OVy91.css +0 -1
- package/dist/web/assets/index-BpL_Kl_s.js +0 -892
- package/dist/web/assets/overview-CDnVKlOt.js +0 -6
- package/dist/web/assets/preview-BEC1E9MK.js +0 -1
- package/dist/web/assets/studio-process-EYQgikxq.js +0 -2
- package/dist/web/assets/timeline-events-Chj2Gbx7.js +0 -1
- package/dist/web/assets/work-turns-CT9RVcWY.js +0 -1
- package/vendor/rho-workspace-plugin-agent-0.4.30.tgz +0 -0
- package/vendor/rho-workspace-plugin-kernel-0.5.5.tgz +0 -0
- package/vendor/rho-workspace-plugin-tables-0.5.0.tgz +0 -0
package/dist/conversations.js
CHANGED
|
@@ -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
|
|
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
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
560
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
:
|
|
842
|
-
? new GenericAcpConversation({ ...common, command
|
|
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
|
|
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 =>
|
|
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
|
|
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
|
+
}
|