@rho-workspace/rho 0.5.4 → 0.5.6

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 (161) hide show
  1. package/dist/agent-connect.d.ts +8 -0
  2. package/dist/agent-connect.js +15 -0
  3. package/dist/browser-access.js +3 -2
  4. package/dist/capabilities.d.ts +2 -2
  5. package/dist/capabilities.js +6 -1
  6. package/dist/cli.js +219 -176
  7. package/dist/contract.generated.d.ts +472 -4
  8. package/dist/contract.generated.js +1 -1
  9. package/dist/contracts/owners.generated.js +455 -6
  10. package/dist/conversations.d.ts +157 -4
  11. package/dist/conversations.js +69 -6
  12. package/dist/discovery.d.ts +1 -0
  13. package/dist/discovery.js +3 -3
  14. package/dist/extension-contract.d.ts +99 -0
  15. package/dist/extension-contract.js +52 -0
  16. package/dist/extension-directory.d.ts +9 -0
  17. package/dist/extension-directory.js +11 -0
  18. package/dist/extension-worker.d.ts +1 -0
  19. package/dist/extension-worker.js +36 -0
  20. package/dist/extensions.d.ts +157 -0
  21. package/dist/extensions.js +349 -0
  22. package/dist/plugin-cli.d.ts +1 -0
  23. package/dist/plugin-cli.js +139 -0
  24. package/dist/plugin-inventory.json +18 -0
  25. package/dist/plugins.d.ts +3 -3
  26. package/dist/project-work.d.ts +3 -1
  27. package/dist/project-work.js +15 -2
  28. package/dist/protocol.d.ts +8 -0
  29. package/dist/protocol.js +16 -7
  30. package/dist/server.d.ts +5 -10
  31. package/dist/server.js +122 -12
  32. package/dist/web/.vite/manifest.json +67 -67
  33. package/dist/web/assets/{canvas-BXhC8RDZ.js → canvas-D8rDdfDz.js} +1 -1
  34. package/dist/web/assets/choices-D-YfnFP6.js +1 -0
  35. package/dist/web/assets/{console-Dbdo-5oO.js → console-Trqc0swD.js} +1 -1
  36. package/dist/web/assets/{dist-B2V0VDO0.js → dist-DZmszrjG.js} +2 -2
  37. package/dist/web/assets/dist-DpIHRk3B.js +1 -0
  38. package/dist/web/assets/{dist-D25Vn17i.js → dist-eMx6b8N1.js} +1 -1
  39. package/dist/web/assets/dist-pF902jHA.js +3 -0
  40. package/dist/web/assets/explore-in-BCHqjJ7B.js +1 -0
  41. package/dist/web/assets/graph-lENgfQNH.js +2 -0
  42. package/dist/web/assets/index-B3_OVy91.css +1 -0
  43. package/dist/web/assets/{index-BHQIaDsx.js → index-BpL_Kl_s.js} +87 -86
  44. package/dist/web/assets/{notes-meaning-DdPbtNzJ.js → notes-meaning-DcVGUBBY.js} +1 -1
  45. package/dist/web/assets/overview-CDnVKlOt.js +6 -0
  46. package/dist/web/assets/preview-BEC1E9MK.js +1 -0
  47. package/dist/web/assets/{review-CmWou4Iv.js → review-DYP0aCXb.js} +1 -1
  48. package/dist/web/assets/{skills-Dy7n3L9d.js → skills-1ec0Z-jx.js} +1 -1
  49. package/dist/web/assets/{skills-work-Cj0W6b-I.js → skills-work-DNexV4OJ.js} +1 -1
  50. package/dist/web/assets/{studio-process-QOY_n2_8.js → studio-process-EYQgikxq.js} +2 -2
  51. package/dist/web/assets/{timeline-events-DmX1BCh3.js → timeline-events-Chj2Gbx7.js} +1 -1
  52. package/dist/web/assets/{work-turns-Co0C71rJ.js → work-turns-CT9RVcWY.js} +1 -1
  53. package/dist/web/assets/workspace-COmCbGV3.js +5 -0
  54. package/dist/web/composition.json +65 -65
  55. package/dist/web/index.html +3 -3
  56. package/dist/web/renderer.json +1 -1
  57. package/dist/web-assets.d.ts +2 -1
  58. package/dist/web-assets.js +3 -2
  59. package/dist/work-contract.js +2 -2
  60. package/docs/FRONTEND.md +26 -18
  61. package/docs/HEADLESS.md +38 -28
  62. package/docs/PLUGIN-AUTHORING.md +4 -0
  63. package/docs/THIRD-PARTY-PLUGINS.md +190 -0
  64. package/node_modules/@rho-workspace/plugin-agent/README.md +68 -0
  65. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +1 -0
  66. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +9 -1
  67. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.d.ts +1 -0
  68. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +10 -0
  69. package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.d.ts +1 -0
  70. package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.js +2 -2
  71. package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.d.ts +6 -0
  72. package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.js +174 -0
  73. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.d.ts +82 -0
  74. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +20 -0
  75. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +84 -0
  76. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.js +190 -0
  77. package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +373 -0
  78. package/node_modules/@rho-workspace/plugin-agent/dist/index.js +1 -0
  79. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +70 -0
  80. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.js +538 -0
  81. package/node_modules/@rho-workspace/plugin-agent/dist/operations.d.ts +33 -0
  82. package/node_modules/@rho-workspace/plugin-agent/dist/operations.js +3 -0
  83. package/node_modules/@rho-workspace/plugin-agent/dist/presentation.d.ts +14 -0
  84. package/node_modules/@rho-workspace/plugin-agent/dist/presentation.js +2 -0
  85. package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.d.ts +15 -0
  86. package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.js +37 -0
  87. package/node_modules/@rho-workspace/plugin-agent/package.json +6 -1
  88. package/node_modules/hono/dist/adapter/aws-lambda/handler.js +1 -1
  89. package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +1 -1
  90. package/node_modules/hono/dist/cjs/adapter/bun/serve-static.js +1 -1
  91. package/node_modules/hono/dist/cjs/adapter/bun/websocket.js +1 -1
  92. package/node_modules/hono/dist/cjs/client/fetch-result-please.js +1 -1
  93. package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +2 -1
  94. package/node_modules/hono/dist/cjs/helper/ssg/ssg.js +5 -3
  95. package/node_modules/hono/dist/cjs/jsx/base.js +49 -30
  96. package/node_modules/hono/dist/cjs/jsx/components.js +2 -2
  97. package/node_modules/hono/dist/cjs/jsx/constants.js +2 -0
  98. package/node_modules/hono/dist/cjs/jsx/jsx-runtime.js +13 -17
  99. package/node_modules/hono/dist/cjs/middleware/etag/index.js +3 -1
  100. package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +1 -0
  101. package/node_modules/hono/dist/cjs/middleware/timing/timing.js +4 -3
  102. package/node_modules/hono/dist/cjs/utils/cookie.js +2 -2
  103. package/node_modules/hono/dist/cjs/utils/mime.js +1 -1
  104. package/node_modules/hono/dist/cjs/utils/url.js +1 -1
  105. package/node_modules/hono/dist/client/fetch-result-please.js +1 -1
  106. package/node_modules/hono/dist/helper/accepts/accepts.js +2 -1
  107. package/node_modules/hono/dist/helper/ssg/ssg.js +5 -3
  108. package/node_modules/hono/dist/jsx/base.js +49 -32
  109. package/node_modules/hono/dist/jsx/components.js +2 -2
  110. package/node_modules/hono/dist/jsx/constants.js +2 -1
  111. package/node_modules/hono/dist/jsx/jsx-runtime.js +15 -19
  112. package/node_modules/hono/dist/middleware/etag/index.js +3 -1
  113. package/node_modules/hono/dist/middleware/language/language.js +1 -1
  114. package/node_modules/hono/dist/middleware/pretty-json/index.js +1 -0
  115. package/node_modules/hono/dist/middleware/timing/timing.js +4 -3
  116. package/node_modules/hono/dist/utils/cookie.js +2 -2
  117. package/node_modules/hono/dist/utils/mime.js +1 -1
  118. package/node_modules/hono/dist/utils/url.js +1 -1
  119. package/node_modules/hono/package.json +1 -1
  120. package/package.json +5 -3
  121. package/protocol/index.json +93 -33
  122. package/protocol/operations/agent.sessions.list.json +146 -0
  123. package/protocol/operations/agent.sessions.read.json +315 -0
  124. package/protocol/operations/conversation.configure.json +29 -0
  125. package/protocol/operations/conversation.decide.json +29 -0
  126. package/protocol/operations/conversation.list.json +29 -0
  127. package/protocol/operations/conversation.permission.json +29 -0
  128. package/protocol/operations/conversation.read.json +29 -0
  129. package/protocol/operations/conversation.restore.json +29 -0
  130. package/protocol/operations/conversation.send.json +29 -0
  131. package/protocol/operations/conversation.settings.check.json +92 -0
  132. package/protocol/operations/conversation.settings.read.json +92 -0
  133. package/protocol/operations/conversation.settings.update.json +92 -0
  134. package/protocol/operations/conversation.start.json +46 -1
  135. package/protocol/operations/conversation.stop.json +29 -0
  136. package/protocol/operations/conversation.trash.json +29 -0
  137. package/protocol/operations/extension.call.json +132 -0
  138. package/protocol/operations/extension.content.json +78 -0
  139. package/protocol/operations/extension.describe.json +195 -0
  140. package/protocol/operations/extension.install.json +198 -0
  141. package/protocol/operations/extension.list.json +123 -0
  142. package/protocol/operations/extension.manage.json +55 -0
  143. package/protocol/operations/extension.read.json +112 -0
  144. package/protocol/operations/observation.snapshot.json +29 -0
  145. package/protocol/operations/project.work.read.json +3 -2
  146. package/protocol/operations/session.grant.json +9 -0
  147. package/protocol/operations/session.instructions.json +43 -0
  148. package/scripts/third-party-acceptance.mjs +77 -0
  149. package/vendor/composition.json +83 -83
  150. package/vendor/rho-workspace-plugin-agent-0.4.30.tgz +0 -0
  151. package/web/theme.css +12 -0
  152. package/dist/web/assets/choices-DU9kHlyQ.js +0 -1
  153. package/dist/web/assets/dist-BV3hWfn_.js +0 -3
  154. package/dist/web/assets/dist-CLNF2g-E.js +0 -1
  155. package/dist/web/assets/explore-in-8Q5ftt-E.js +0 -1
  156. package/dist/web/assets/graph-hS2kvHTI.js +0 -2
  157. package/dist/web/assets/index-Cj1_iqyK.css +0 -1
  158. package/dist/web/assets/overview-BV6ZO5TO.js +0 -6
  159. package/dist/web/assets/preview-n5lOOdjN.js +0 -1
  160. package/dist/web/assets/workspace-BKFefRfJ.js +0 -5
  161. package/vendor/rho-workspace-plugin-agent-0.4.28.tgz +0 -0
@@ -165,6 +165,22 @@ export interface ConversationPorts {
165
165
  listed: string;
166
166
  } | null>;
167
167
  notebook?(): boolean;
168
+ nativeSession?(project: Project, producer: 'claude-code' | 'codex', sessionId: string): Promise<{
169
+ open: {
170
+ where: string;
171
+ since?: string;
172
+ pid?: number;
173
+ } | null;
174
+ lastAt: string;
175
+ } | null>;
176
+ }
177
+ export interface ContinueSession {
178
+ producer: 'claude-code' | 'codex' | 'dsh';
179
+ sessionId: string;
180
+ title: string;
181
+ where: string;
182
+ copy: boolean;
183
+ backend: 'claude-acp' | 'codex-acp' | 'acp';
168
184
  }
169
185
  export interface DescribeRequest {
170
186
  path: string;
@@ -256,6 +272,11 @@ export declare class Conversations {
256
272
  costUsd?: number | null;
257
273
  sessionId?: string | null;
258
274
  trashedAt?: string | null;
275
+ origin?: {
276
+ producer: string;
277
+ where: string;
278
+ copyOf?: string;
279
+ } | null;
259
280
  }[]>;
260
281
  private relist;
261
282
  reviewEvidence(project: Project, annotationId: string): Promise<{
@@ -319,6 +340,11 @@ export declare class Conversations {
319
340
  costUsd?: number | null;
320
341
  sessionId?: string | null;
321
342
  trashedAt?: string | null;
343
+ origin?: {
344
+ producer: string;
345
+ where: string;
346
+ copyOf?: string;
347
+ } | null;
322
348
  };
323
349
  }>;
324
350
  restore(project: Project, input: {
@@ -356,6 +382,11 @@ export declare class Conversations {
356
382
  costUsd?: number | null;
357
383
  sessionId?: string | null;
358
384
  trashedAt?: string | null;
385
+ origin?: {
386
+ producer: string;
387
+ where: string;
388
+ copyOf?: string;
389
+ } | null;
359
390
  };
360
391
  }>;
361
392
  delete(project: Project, by: string, input: {
@@ -401,6 +432,11 @@ export declare class Conversations {
401
432
  costUsd?: number | null;
402
433
  sessionId?: string | null;
403
434
  trashedAt?: string | null;
435
+ origin?: {
436
+ producer: string;
437
+ where: string;
438
+ copyOf?: string;
439
+ } | null;
404
440
  };
405
441
  observation: SessionObservation | null;
406
442
  config: Choices;
@@ -410,7 +446,7 @@ export declare class Conversations {
410
446
  private sessionObservation;
411
447
  work(project: Project, limit?: number): Promise<{
412
448
  sessions: {
413
- backend: "acp" | "claude-acp" | "codex-acp" | "claude-cli";
449
+ backend: "claude-acp" | "codex-acp" | "acp" | "claude-cli";
414
450
  sessionId: string | null;
415
451
  observation: SessionObservation | null;
416
452
  state: string;
@@ -442,6 +478,11 @@ export declare class Conversations {
442
478
  } | null;
443
479
  costUsd?: number | null;
444
480
  trashedAt?: string | null;
481
+ origin?: {
482
+ producer: string;
483
+ where: string;
484
+ copyOf?: string;
485
+ } | null;
445
486
  }[];
446
487
  omitted: number;
447
488
  }>;
@@ -451,6 +492,7 @@ export declare class Conversations {
451
492
  contexts?: string[];
452
493
  backend?: Backend;
453
494
  config?: Choices;
495
+ continue?: ContinueSession;
454
496
  }): Promise<{
455
497
  conversation: {
456
498
  state: string;
@@ -484,9 +526,18 @@ export declare class Conversations {
484
526
  costUsd?: number | null;
485
527
  sessionId?: string | null;
486
528
  trashedAt?: string | null;
529
+ origin?: {
530
+ producer: string;
531
+ where: string;
532
+ copyOf?: string;
533
+ } | null;
487
534
  };
488
535
  turn: Turn;
489
536
  }>;
537
+ /** Whether a process holding a session open is Rho's own connection to this conversation: the
538
+ * Agent its adapter started. */
539
+ owns(project: Project, conversationId: string, pid: number | undefined): Promise<boolean>;
540
+ private notHeld;
490
541
  send(project: Project, by: string, input: {
491
542
  conversationId: string;
492
543
  changeId: string;
@@ -526,6 +577,11 @@ export declare class Conversations {
526
577
  costUsd?: number | null;
527
578
  sessionId?: string | null;
528
579
  trashedAt?: string | null;
580
+ origin?: {
581
+ producer: string;
582
+ where: string;
583
+ copyOf?: string;
584
+ } | null;
529
585
  };
530
586
  turn: Turn;
531
587
  }>;
@@ -566,6 +622,11 @@ export declare class Conversations {
566
622
  costUsd?: number | null;
567
623
  sessionId?: string | null;
568
624
  trashedAt?: string | null;
625
+ origin?: {
626
+ producer: string;
627
+ where: string;
628
+ copyOf?: string;
629
+ } | null;
569
630
  };
570
631
  observation: SessionObservation | null;
571
632
  config: Choices;
@@ -574,17 +635,43 @@ export declare class Conversations {
574
635
  private offers;
575
636
  private readSettings;
576
637
  private settingsView;
638
+ private agents;
577
639
  settings(): Promise<{
578
640
  version: number;
579
641
  updatedAt: string | null;
580
642
  backend: Backend | null;
581
- serviceBackend: "acp" | "claude-acp" | "codex-acp" | "claude-cli";
643
+ serviceBackend: "claude-acp" | "codex-acp" | "acp" | "claude-cli";
582
644
  choices: Partial<Record<Backend, Choices>>;
583
645
  offered: Partial<Record<Backend, Offered>>;
584
646
  checking: Backend[];
585
647
  describe: {
586
648
  enabled: boolean;
587
649
  };
650
+ agents: {
651
+ 'claude-acp': {
652
+ found: boolean;
653
+ problem: string;
654
+ } | {
655
+ found: boolean;
656
+ problem?: undefined;
657
+ };
658
+ 'codex-acp': {
659
+ found: boolean;
660
+ problem: string;
661
+ } | {
662
+ found: boolean;
663
+ problem?: undefined;
664
+ };
665
+ acp: {
666
+ found: boolean;
667
+ name: string | null;
668
+ problem?: undefined;
669
+ } | {
670
+ found: boolean;
671
+ problem: string;
672
+ name?: undefined;
673
+ };
674
+ };
588
675
  }>;
589
676
  updateSettings(input: {
590
677
  expectedVersion: number;
@@ -597,13 +684,38 @@ export declare class Conversations {
597
684
  version: number;
598
685
  updatedAt: string | null;
599
686
  backend: Backend | null;
600
- serviceBackend: "acp" | "claude-acp" | "codex-acp" | "claude-cli";
687
+ serviceBackend: "claude-acp" | "codex-acp" | "acp" | "claude-cli";
601
688
  choices: Partial<Record<Backend, Choices>>;
602
689
  offered: Partial<Record<Backend, Offered>>;
603
690
  checking: Backend[];
604
691
  describe: {
605
692
  enabled: boolean;
606
693
  };
694
+ agents: {
695
+ 'claude-acp': {
696
+ found: boolean;
697
+ problem: string;
698
+ } | {
699
+ found: boolean;
700
+ problem?: undefined;
701
+ };
702
+ 'codex-acp': {
703
+ found: boolean;
704
+ problem: string;
705
+ } | {
706
+ found: boolean;
707
+ problem?: undefined;
708
+ };
709
+ acp: {
710
+ found: boolean;
711
+ name: string | null;
712
+ problem?: undefined;
713
+ } | {
714
+ found: boolean;
715
+ problem: string;
716
+ name?: undefined;
717
+ };
718
+ };
607
719
  }>;
608
720
  check(project: Project, input: {
609
721
  backend: Backend;
@@ -611,13 +723,38 @@ export declare class Conversations {
611
723
  version: number;
612
724
  updatedAt: string | null;
613
725
  backend: Backend | null;
614
- serviceBackend: "acp" | "claude-acp" | "codex-acp" | "claude-cli";
726
+ serviceBackend: "claude-acp" | "codex-acp" | "acp" | "claude-cli";
615
727
  choices: Partial<Record<Backend, Choices>>;
616
728
  offered: Partial<Record<Backend, Offered>>;
617
729
  checking: Backend[];
618
730
  describe: {
619
731
  enabled: boolean;
620
732
  };
733
+ agents: {
734
+ 'claude-acp': {
735
+ found: boolean;
736
+ problem: string;
737
+ } | {
738
+ found: boolean;
739
+ problem?: undefined;
740
+ };
741
+ 'codex-acp': {
742
+ found: boolean;
743
+ problem: string;
744
+ } | {
745
+ found: boolean;
746
+ problem?: undefined;
747
+ };
748
+ acp: {
749
+ found: boolean;
750
+ name: string | null;
751
+ problem?: undefined;
752
+ } | {
753
+ found: boolean;
754
+ problem: string;
755
+ name?: undefined;
756
+ };
757
+ };
621
758
  }>;
622
759
  private inspect;
623
760
  private offer;
@@ -657,6 +794,11 @@ export declare class Conversations {
657
794
  costUsd?: number | null;
658
795
  sessionId?: string | null;
659
796
  trashedAt?: string | null;
797
+ origin?: {
798
+ producer: string;
799
+ where: string;
800
+ copyOf?: string;
801
+ } | null;
660
802
  };
661
803
  turn: Turn;
662
804
  }>;
@@ -699,6 +841,11 @@ export declare class Conversations {
699
841
  costUsd?: number | null;
700
842
  sessionId?: string | null;
701
843
  trashedAt?: string | null;
844
+ origin?: {
845
+ producer: string;
846
+ where: string;
847
+ copyOf?: string;
848
+ } | null;
702
849
  };
703
850
  turn: Turn;
704
851
  }>;
@@ -740,6 +887,11 @@ export declare class Conversations {
740
887
  costUsd?: number | null;
741
888
  sessionId?: string | null;
742
889
  trashedAt?: string | null;
890
+ origin?: {
891
+ producer: string;
892
+ where: string;
893
+ copyOf?: string;
894
+ } | null;
743
895
  };
744
896
  turn: Turn;
745
897
  }>;
@@ -747,6 +899,7 @@ export declare class Conversations {
747
899
  private retire;
748
900
  private cancelPermissions;
749
901
  private launch;
902
+ private changedElsewhere;
750
903
  private driver;
751
904
  private capture;
752
905
  private observe;
@@ -8,6 +8,7 @@ import path from 'node:path';
8
8
  import { createHash, randomBytes, randomUUID } from 'node:crypto';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { atomicJson, canonical, digest, Fault, now, readJson, requireThat } from '@rho-workspace/plugin-contracts';
11
+ import { acpEnvironment, missingClient } from '@rho-workspace/plugin-agent/acp-command';
11
12
  // Rho holds user messages and their original identities. The selected Agent plugin
12
13
  // owns the connection, native session and protocol. Reads never start a driver.
13
14
  // A new Claude Code session opens in Manual mode unless the person's Claude settings name another.
@@ -234,7 +235,8 @@ export class Conversations {
234
235
  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,
235
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 ?? {},
236
237
  usage: usage ? { used: usage.used, size: usage.size, cost: usage.cost ?? null } : null, costUsd: costs.length ? costs.reduce((a, b) => a + b, 0) : null,
237
- sessionId: c.acpSessionId ?? c.nativeSessionId ?? null, trashedAt: c.trashedAt ?? null };
238
+ sessionId: c.acpSessionId ?? c.nativeSessionId ?? null, trashedAt: c.trashedAt ?? null,
239
+ origin: c.origin ? { producer: c.origin.producer, where: c.origin.where, ...(c.origin.copy ? { copyOf: c.origin.sessionId } : {}) } : null };
238
240
  }
239
241
  async write(project, c) {
240
242
  const directory = await this.folder(project);
@@ -394,7 +396,9 @@ export class Conversations {
394
396
  requireThat(!input.backend || backends.includes(input.backend), 'invalid', 'Choose Claude Code or Codex.');
395
397
  if (input.config)
396
398
  this.choices(input.config);
397
- 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 } : {}) }));
399
+ const from = input.continue;
400
+ 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 } } : {}) }));
398
402
  return this.serial(project.id, async () => {
399
403
  const index = await this.index(project), existing = index.conversations.find(x => x.changeId === input.changeId);
400
404
  requireThat(!index.deleted?.some(x => x.changeId === input.changeId), 'conflict', 'That request started a conversation that was later deleted. It is not started again.');
@@ -404,13 +408,26 @@ export class Conversations {
404
408
  return this.reply(project, c, c.turns[0]);
405
409
  }
406
410
  requireThat(index.conversations.filter(x => !x.trashedAt).length < this.limits.maxConversations, 'limit', `This project keeps at most ${this.limits.maxConversations} conversations started in Rho. Move one to the trash to start another.`, { limit: 'maxConversations', maximum: this.limits.maxConversations });
411
+ if (from && !from.copy) {
412
+ const already = index.conversations.find(x => x.sessionId === from.sessionId);
413
+ requireThat(!already, 'conflict', already?.trashedAt ? 'This session is a conversation in the trash. Restore it to continue.' : 'This session already continues as a conversation here. Open it to send your message.');
414
+ await this.notHeld(project, from.producer, from.sessionId, false);
415
+ }
407
416
  this.capacity();
408
417
  const saved = await this.readSettings();
409
- const backend = input.backend ?? saved.backend ?? this.config.backend ?? 'claude-acp';
418
+ const backend = from?.backend ?? input.backend ?? saved.backend ?? this.config.backend ?? 'claude-acp';
410
419
  const config = backend === 'claude-cli' ? undefined : input.config ?? saved.choices[backend];
411
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 } : {}) };
412
421
  for (const id of input.contexts ?? [])
413
422
  await this.ports.context?.(project, id);
423
+ if (from) {
424
+ c.origin = { producer: from.producer, sessionId: from.sessionId, where: from.where, copy: from.copy };
425
+ if (from.copy)
426
+ c.forkFrom = from.sessionId;
427
+ else
428
+ c.acpSessionId = from.sessionId;
429
+ c.title = from.title;
430
+ }
414
431
  const turn = this.turn(input.changeId, requestDigest, by, text, 1);
415
432
  if (input.contexts?.length)
416
433
  turn.contexts = [...input.contexts];
@@ -421,6 +438,29 @@ export class Conversations {
421
438
  return this.reply(project, c, turn);
422
439
  });
423
440
  }
441
+ /** Whether a process holding a session open is Rho's own connection to this conversation: the
442
+ * Agent its adapter started. */
443
+ async owns(project, conversationId, pid) {
444
+ const adapter = this.drivers.get(project.id + '/' + conversationId)?.driver?.pid;
445
+ if (!adapter || !pid || process.platform === 'win32')
446
+ return false;
447
+ const { execFile } = await import('node:child_process');
448
+ let current = pid;
449
+ for (let depth = 0; depth < 4 && current > 1; depth++) {
450
+ const parent = await new Promise(resolve => execFile('ps', ['-o', 'ppid=', '-p', String(current)], { timeout: 2000 }, (error, out) => resolve(error ? 0 : Number(String(out).trim()))));
451
+ if (parent === adapter)
452
+ return true;
453
+ current = parent;
454
+ }
455
+ return false;
456
+ }
457
+ async notHeld(project, producer, sessionId, existing, conversationId) {
458
+ if (producer !== 'claude-code' && producer !== 'codex')
459
+ return;
460
+ const found = await this.ports.nativeSession?.(project, producer, sessionId) ?? null;
461
+ const holder = found?.open && !(conversationId && await this.owns(project, conversationId, found.open.pid)) ? found.open : null;
462
+ 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
+ }
424
464
  async send(project, by, input) {
425
465
  const text = this.message(input.text), requestDigest = digest(canonical({ text, ...(input.contexts?.length ? { contexts: input.contexts } : {}), ...(input.continuationOf ? { continuationOf: input.continuationOf } : {}) }));
426
466
  return this.serial(project.id, async () => {
@@ -444,6 +484,9 @@ export class Conversations {
444
484
  requireThat(!alive, 'conflict', `The Agent from before Rho restarted may still be answering (process ${last.pid}). End that process before sending another message.`);
445
485
  }
446
486
  requireThat(c.turns.length < this.limits.maxTurns, 'limit', `A conversation keeps at most ${this.limits.maxTurns} messages. Start a new conversation.`, { limit: 'maxTurns', maximum: this.limits.maxTurns });
487
+ // A session continued in a terminal or another app may be open there now.
488
+ if (c.acpSessionId && (c.client === 'claude-code' || c.client === 'codex'))
489
+ await this.notHeld(project, c.client, c.acpSessionId, true, c.id);
447
490
  requireThat(Buffer.byteLength(JSON.stringify({ ...c, observation: undefined })) + this.limits.maxObservationBytes + this.limits.maxTurnBytes + this.limits.maxMessageBytes <= this.limits.maxRecordBytes, 'limit', 'This conversation has reached the size Rho keeps. Start a new conversation.', { limit: 'maxRecordBytes', maximum: this.limits.maxRecordBytes });
448
491
  this.capacity();
449
492
  for (const old of c.turns)
@@ -510,7 +553,19 @@ export class Conversations {
510
553
  }
511
554
  settingsView(s) {
512
555
  const service = this.config.backend ?? 'claude-acp';
513
- 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 } };
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() };
557
+ }
558
+ // 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() {
561
+ const found = (problem) => problem ? { found: false, problem } : { found: true };
562
+ const command = this.config.acpCommand;
563
+ return {
564
+ // A command the embedding names is its own choice and is not looked for, as the adapters do.
565
+ 'claude-acp': found(this.config.claudeAcp?.command ? null : missingClient('claude', acpEnvironment('claude', { ...process.env }, this.binary))),
566
+ '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.' }
568
+ };
514
569
  }
515
570
  async settings() { return this.settingsView(await this.readSettings()); }
516
571
  // The person's Agent and settings for new conversations. Each Agent's choices are replaced as a
@@ -733,12 +788,20 @@ export class Conversations {
733
788
  }
734
789
  });
735
790
  }
791
+ async changedElsewhere(project, c) {
792
+ if (!c.acpSessionId || (c.client !== 'claude-code' && c.client !== 'codex'))
793
+ return false;
794
+ const previous = c.turns.at(-2), found = await this.ports.nativeSession?.(project, c.client, c.acpSessionId).catch(() => null);
795
+ return !!found && !!previous?.endedAt && Date.parse(found.lastAt) > Date.parse(previous.endedAt) + 5000;
796
+ }
736
797
  async driver(project, c) {
737
798
  return this.serial('driver-pool', async () => {
738
799
  const key = project.id + '/' + c.id, old = this.drivers.get(key);
739
800
  if (old?.timer)
740
801
  clearTimeout(old.timer);
741
- if (old?.driver.connected)
802
+ // An open connection keeps the session as it last saw it. When the session went on in
803
+ // another window since, it is opened again from the Agent's own record.
804
+ if (old?.driver.connected && !await this.changedElsewhere(project, c))
742
805
  return old.driver;
743
806
  if (old)
744
807
  await this.retire(key);
@@ -770,7 +833,7 @@ export class Conversations {
770
833
  } };
771
834
  // Every session started here is told about the project's shared notes and its section.
772
835
  const instructions = notesInstructions(c.id, { notebook: !!this.ports.notebook?.() });
773
- const common = { cwd: project.root, mcpServers, limits: this.limits, sessionId: c.acpSessionId, config: c.config, instructions, ...observed };
836
+ const common = { cwd: project.root, mcpServers, limits: this.limits, sessionId: c.acpSessionId, ...(!c.acpSessionId && c.forkFrom ? { forkFrom: c.forkFrom } : {}), config: c.config, instructions, ...observed };
774
837
  const driver = c.backend === 'codex-acp'
775
838
  ? new CodexAcpConversation({ ...this.config.codexAcp, ...common, codex: this.config.codex })
776
839
  : c.backend === 'claude-acp'
@@ -1,5 +1,6 @@
1
1
  import type { Principal, Project } from './state.js';
2
2
  import type { Plugins } from './plugins.js';
3
+ export declare const applicationEffects: Record<string, string>;
3
4
  export declare class Discovery {
4
5
  plugins: Plugins;
5
6
  project: Project;
package/dist/discovery.js CHANGED
@@ -3,11 +3,11 @@ import { Capabilities, rhoWorkspaceSkill } from './capabilities.js';
3
3
  import { protocol } from './protocol.js';
4
4
  import { schemaClosure } from './protocol-discovery.js';
5
5
  const size = (v) => Buffer.byteLength(JSON.stringify(v)) + 64;
6
- const applicationEffects = Object.fromEntries([
7
- ...['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']),
6
+ export const applicationEffects = Object.fromEntries([
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
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']),
9
9
  ...['workspace.save', 'workspace.export', 'workspace.restore', 'action.export'].map(m => [m, 'file']),
10
- ...['action.retry', 'action.cancel'].map(m => [m, 'native']),
10
+ ...['extension.call', 'extension.install', 'extension.manage', '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']);
@@ -0,0 +1,99 @@
1
+ export declare const extensionOperations: {
2
+ 'extension.list': {
3
+ type: string;
4
+ properties: Record<string, unknown>;
5
+ required: string[];
6
+ additionalProperties: boolean;
7
+ };
8
+ 'extension.describe': {
9
+ type: string;
10
+ properties: Record<string, unknown>;
11
+ required: string[];
12
+ additionalProperties: boolean;
13
+ };
14
+ 'extension.content': {
15
+ type: string;
16
+ properties: Record<string, unknown>;
17
+ required: string[];
18
+ additionalProperties: boolean;
19
+ };
20
+ 'extension.call': {
21
+ type: string;
22
+ properties: Record<string, unknown>;
23
+ required: string[];
24
+ additionalProperties: boolean;
25
+ };
26
+ 'extension.read': {
27
+ type: string;
28
+ properties: Record<string, unknown>;
29
+ required: string[];
30
+ additionalProperties: boolean;
31
+ };
32
+ 'extension.install': {
33
+ type: string;
34
+ properties: Record<string, unknown>;
35
+ required: string[];
36
+ additionalProperties: boolean;
37
+ };
38
+ 'extension.manage': {
39
+ type: string;
40
+ properties: Record<string, unknown>;
41
+ required: string[];
42
+ additionalProperties: boolean;
43
+ };
44
+ };
45
+ export declare const extensionResponses: {
46
+ 'extension.list': {
47
+ type: string;
48
+ properties: Record<string, unknown>;
49
+ required: string[];
50
+ additionalProperties: boolean;
51
+ };
52
+ 'extension.describe': {
53
+ type: string;
54
+ properties: Record<string, unknown>;
55
+ required: string[];
56
+ additionalProperties: boolean;
57
+ };
58
+ 'extension.install': {
59
+ type: string;
60
+ properties: Record<string, unknown>;
61
+ required: string[];
62
+ additionalProperties: boolean;
63
+ };
64
+ 'extension.content': {
65
+ type: string;
66
+ properties: Record<string, unknown>;
67
+ required: string[];
68
+ additionalProperties: boolean;
69
+ };
70
+ 'extension.call': {
71
+ type: string;
72
+ properties: Record<string, unknown>;
73
+ required: string[];
74
+ additionalProperties: boolean;
75
+ };
76
+ 'extension.read': {
77
+ type: string;
78
+ properties: Record<string, unknown>;
79
+ required: string[];
80
+ additionalProperties: boolean;
81
+ };
82
+ 'extension.manage': {
83
+ type: string;
84
+ properties: Record<string, unknown>;
85
+ required: string[];
86
+ additionalProperties: boolean;
87
+ };
88
+ };
89
+ export declare const extensionDescriptions: {
90
+ 'extension.list': string;
91
+ 'extension.describe': string;
92
+ 'extension.content': string;
93
+ 'extension.call': string;
94
+ 'extension.read': string;
95
+ 'extension.install': string;
96
+ 'extension.manage': string;
97
+ };
98
+ export declare const extensionManagement: string[];
99
+ export declare const extensionMethods: string[];
@@ -0,0 +1,52 @@
1
+ // Installed third-party packages are application assembly, independent of the
2
+ // functional owners compiled into Rho. This protocol never imports their code.
3
+ const str = { type: 'string', minLength: 1, maxLength: 1024 };
4
+ const id = { type: 'string', pattern: '^[a-z0-9][a-z0-9._-]{0,95}$' };
5
+ const sha = { type: 'string', pattern: '^[a-f0-9]{64}$' };
6
+ const object = (properties, required = Object.keys(properties)) => ({ type: 'object', properties, required, additionalProperties: false });
7
+ const json = { type: 'object', additionalProperties: true };
8
+ const project = { projectId: str };
9
+ const selected = { ...project, pluginId: id, expectedDigest: sha };
10
+ export const extensionOperations = {
11
+ 'extension.list': object(project, []),
12
+ 'extension.describe': object({ ...project, pluginId: id }),
13
+ 'extension.content': object({ ...selected, kind: { enum: ['skill', 'view'] }, entryId: id }),
14
+ 'extension.call': object({ ...selected, operation: id, input: json, changeId: { type: 'string', pattern: '^[A-Za-z0-9._:@-]{1,128}$' } }),
15
+ 'extension.read': object({ ...project, invocationId: sha }),
16
+ 'extension.install': object({ source: str, replace: id, expectedDigest: sha }, ['source']),
17
+ 'extension.manage': object({ pluginId: id, expectedDigest: sha, action: { enum: ['enable', 'disable', 'remove'] } }),
18
+ };
19
+ const array = (items) => ({ type: 'array', items });
20
+ const text = { type: 'string' };
21
+ const entry = { ...object({ id, title: text, file: text }), additionalProperties: true };
22
+ const manifest = { ...object({ apiVersion: { const: 1 }, id, title: text, description: text, host: text,
23
+ operations: array({ ...object({ id, title: text, description: text, effect: { enum: ['read', 'native'] }, methods: array(text), inputSchema: json }), additionalProperties: true }),
24
+ skills: array(entry), views: array(entry),
25
+ }, ['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');
28
+ const descriptor = object({ ...installed, manifest }, [...installedRequired, 'manifest']);
29
+ const receipt = object({ invocationId: sha, caller: text, projectId: text, pluginId: id, packageDigest: sha, operation: id, requestDigest: sha,
30
+ state: { enum: ['pending', 'completed', 'unknown'] }, startedAt: text, endedAt: text, value: {}, error: text,
31
+ delegations: array(object({ method: text, request: {}, state: { enum: ['pending', 'completed', 'unknown'] }, value: {}, error: text }, ['method', 'request', 'state'])),
32
+ }, ['invocationId', 'caller', 'projectId', 'pluginId', 'packageDigest', 'operation', 'requestDigest', 'state', 'startedAt']);
33
+ 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 }),
35
+ 'extension.describe': descriptor,
36
+ 'extension.install': descriptor,
37
+ 'extension.content': object({ pluginId: id, packageDigest: sha, entryId: id, kind: { enum: ['skill', 'view'] }, title: text, text }),
38
+ 'extension.call': receipt,
39
+ 'extension.read': receipt,
40
+ 'extension.manage': object({ pluginId: id, action: { enum: ['enable', 'disable', 'remove'] }, retained: text }),
41
+ };
42
+ 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.',
44
+ '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
+ 'extension.content': 'Read one installed plugin Skill or standalone HTML view at its selected package digest. Reading starts no plugin or runtime.',
46
+ '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.',
47
+ 'extension.read': 'Observe an original plugin invocation, including pending or unknown outcomes. Never reruns code. Only its original caller can read it.',
48
+ '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
+ '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
+ };
51
+ export const extensionManagement = ['extension.install', 'extension.manage'];
52
+ export const extensionMethods = Object.keys(extensionOperations);
@@ -0,0 +1,9 @@
1
+ export declare const extensionDirectory: {
2
+ id: string;
3
+ title: string;
4
+ version: string;
5
+ description: string;
6
+ source: string;
7
+ repository: string;
8
+ needs: string;
9
+ }[];
@@ -0,0 +1,11 @@
1
+ // Optional discovery, not a registration requirement. Packages keep their own
2
+ // repositories/releases; an entry records the source a maintainer actually checked.
3
+ export const extensionDirectory = [
4
+ {
5
+ id: 'yulab.sclet', title: 'sclet', version: '0.1.0',
6
+ description: 'State-aware single-cell analysis using sclet in your shared R Notebook.',
7
+ source: 'git+https://github.com/YuLab-SMU/Rho-plugins.git#82df99f8cbab868c30949692172c6cf11db2f5c0:examples/sclet-plugin',
8
+ repository: 'https://github.com/YuLab-SMU/Rho-plugins/tree/82df99f8cbab868c30949692172c6cf11db2f5c0/examples/sclet-plugin',
9
+ needs: 'R, sclet, SingleCellExperiment and jsonlite in the selected R environment.',
10
+ },
11
+ ];
@@ -0,0 +1 @@
1
+ export {};