@chainingintention/pi-web-cn 1.202610.4 → 2.202610.0

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 (299) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +31 -7
  3. package/dist/cli.js.map +1 -1
  4. package/dist/client/assets/PiWebApp-BXSziMEg.js +4921 -0
  5. package/dist/client/assets/classic-CyM97Akm.css +54 -0
  6. package/dist/client/assets/external-B9K8duyP.js +2 -0
  7. package/dist/client/assets/graphite-DquaqFXU.css +54 -0
  8. package/dist/client/assets/index-cdtUOfOW.js +2 -0
  9. package/dist/client/assets/paper-CB1oLuRr.css +54 -0
  10. package/dist/client/assets/pi-web-dark-D_KtbM95.css +54 -0
  11. package/dist/client/assets/pi-web-light-DSNLMIBI.css +54 -0
  12. package/dist/client/assets/vendor-editor-core-BfnHL1Dg.js +10 -0
  13. package/dist/client/assets/{vendor-editor-languages-DOCw3vc9.js → vendor-editor-languages-bNquDJP0.js} +1 -1
  14. package/dist/client/index.html +3 -47
  15. package/dist/config.js +11 -2
  16. package/dist/config.js.map +1 -1
  17. package/dist/nativeServices/installedServiceDefinitions.js +10 -7
  18. package/dist/nativeServices/installedServiceDefinitions.js.map +1 -1
  19. package/dist/pi-packages/captains-log/dist/browser/index.js +43 -47
  20. package/dist/pi-packages/captains-log/dist/browser/panel.js +6 -10
  21. package/dist/pi-packages/captains-log/dist/companion.js +1 -1
  22. package/dist/pi-packages/captains-log/package.json +4 -4
  23. package/dist/pi-packages/relays/extensions/index.js +61 -0
  24. package/dist/pi-packages/relays/package.json +10 -1
  25. package/dist/pi-packages/relays/pi-web-plugin.js +52 -2
  26. package/dist/pi-packages/relays/prompts/relay-worktree.md +6 -16
  27. package/dist/pi-packages/relays/prompts/relay.md +6 -33
  28. package/dist/pi-packages/relays/relayDispatch.js +67 -0
  29. package/dist/pi-packages/relays/relayIdentity.js +37 -0
  30. package/dist/pi-packages/relays/relaysPanelElement.js +75 -37
  31. package/dist/pi-packages/relays/skills/relay/SKILL.md +24 -68
  32. package/dist/pi-packages/relays/skills/relay-runner/SKILL.md +40 -219
  33. package/dist/pi-packages/workflows/browser/pi-web-plugin.js +42 -0
  34. package/dist/pi-packages/workflows/browser/worktreeDialog.js +170 -0
  35. package/dist/pi-packages/workflows/package.json +23 -0
  36. package/dist/pi-packages/workflows/prompts/worktree.md +21 -0
  37. package/dist/pi-packages/workflows/server-plugin.js +586 -0
  38. package/dist/pi-packages/workflows/vite.config.mjs +17 -0
  39. package/dist/pi-web-plugins/code-highlight/browser/pi-web-plugin.js +22 -0
  40. package/dist/pi-web-plugins/code-highlight/package.json +13 -0
  41. package/dist/pi-web-plugins/files/browser/pi-web-plugin.js +115 -105
  42. package/dist/pi-web-plugins/git/browser/git-panel.js +60 -46
  43. package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +23 -3
  44. package/dist/pi-web-plugins/git/git-backend.js +20 -1
  45. package/dist/pi-web-plugins/git/server-plugin.js +20 -3
  46. package/dist/pi-web-plugins/git/workspace-discovery-notify.cjs +14 -0
  47. package/dist/pi-web-plugins/git/workspaceDiscoveryHooks.js +186 -0
  48. package/dist/pi-web-plugins/info/infoInternals.js +3 -3
  49. package/dist/pi-web-plugins/info/pi-web-plugin.js +1 -1
  50. package/dist/pi-web-plugins/mermaid/browser/mermaid-engine.js +1087 -422
  51. package/dist/pi-web-plugins/mermaid/browser/pi-web-plugin.js +8 -3
  52. package/dist/pi-web-plugins/terminal/browser/pi-web-plugin.js +39 -33
  53. package/dist/pi-web-plugins/updates/pi-web-plugin.js +13 -13
  54. package/dist/pi-web-plugins/updates/updatesLogic.js +4 -5
  55. package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +21 -1
  56. package/dist/pi-web-plugins/workspace-tasks/tasksPanelElement.js +39 -22
  57. package/dist/pi-web-plugins/zh-cn/bundled.json +44 -1
  58. package/dist/pi-web-plugins/zh-cn/cli.json +60 -1
  59. package/dist/pi-web-plugins/zh-cn/client.json +7 -1
  60. package/dist/pi-web-plugins/zh-cn/components.json +252 -2
  61. package/dist/pi-web-plugins/zh-cn/config-errors.json +17 -1
  62. package/dist/pi-web-plugins/zh-cn/files.json +10 -1
  63. package/dist/pi-web-plugins/zh-cn/main.json +1 -1
  64. package/dist/pi-web-plugins/zh-cn/provider-auth.json +2 -1
  65. package/dist/pi-web-plugins/zh-cn/server.json +83 -1
  66. package/dist/piAi.js +4 -0
  67. package/dist/piAi.js.map +1 -0
  68. package/dist/piRuntime.js +150 -0
  69. package/dist/piRuntime.js.map +1 -0
  70. package/dist/piSdk.js +5 -0
  71. package/dist/piSdk.js.map +1 -0
  72. package/dist/piWebUpdate.js +508 -0
  73. package/dist/piWebUpdate.js.map +1 -0
  74. package/dist/piWebVersionReport.js +43 -1
  75. package/dist/piWebVersionReport.js.map +1 -1
  76. package/dist/plugin-api.d.ts +393 -51
  77. package/dist/server/activity/workspaceActivityService.js +8 -1
  78. package/dist/server/activity/workspaceActivityService.js.map +1 -1
  79. package/dist/server/app.js +19 -4
  80. package/dist/server/app.js.map +1 -1
  81. package/dist/server/browserMessageProjection.js +39 -20
  82. package/dist/server/browserMessageProjection.js.map +1 -1
  83. package/dist/server/configRoutes.js +8 -0
  84. package/dist/server/configRoutes.js.map +1 -1
  85. package/dist/server/index.js +10 -1
  86. package/dist/server/index.js.map +1 -1
  87. package/dist/server/knownAutoInstallPiPackages.js +6 -0
  88. package/dist/server/knownAutoInstallPiPackages.js.map +1 -1
  89. package/dist/server/machines/machineProxyRoutes.js +37 -2
  90. package/dist/server/machines/machineProxyRoutes.js.map +1 -1
  91. package/dist/server/navigationPreferenceRoutes.js +59 -0
  92. package/dist/server/navigationPreferenceRoutes.js.map +1 -0
  93. package/dist/server/piPackageService.js +1 -1
  94. package/dist/server/piPackageService.js.map +1 -1
  95. package/dist/server/piWebPluginCatalog.js +1 -1
  96. package/dist/server/piWebPluginCatalog.js.map +1 -1
  97. package/dist/server/piWebPluginService.js +7 -1
  98. package/dist/server/piWebPluginService.js.map +1 -1
  99. package/dist/server/piWebStatus.js +17 -56
  100. package/dist/server/piWebStatus.js.map +1 -1
  101. package/dist/server/plugins/pluginBackendChannelProxyAdmission.js +1 -1
  102. package/dist/server/plugins/pluginBackendChannelProxyAdmission.js.map +1 -1
  103. package/dist/server/plugins/pluginBackendChannelProxyRoutes.js +40 -42
  104. package/dist/server/plugins/pluginBackendChannelProxyRoutes.js.map +1 -1
  105. package/dist/server/plugins/pluginBackendProxyRoutes.js +40 -44
  106. package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -1
  107. package/dist/server/plugins/pluginBackendRegistry.js +66 -35
  108. package/dist/server/plugins/pluginBackendRegistry.js.map +1 -1
  109. package/dist/server/plugins/projectPluginSettings.js +95 -0
  110. package/dist/server/plugins/projectPluginSettings.js.map +1 -0
  111. package/dist/server/plugins/serverPluginCatalogCapability.js +106 -0
  112. package/dist/server/plugins/serverPluginCatalogCapability.js.map +1 -0
  113. package/dist/server/plugins/serverPluginRuntime.js +47 -11
  114. package/dist/server/plugins/serverPluginRuntime.js.map +1 -1
  115. package/dist/server/projectTrustRoutes.js +1 -1
  116. package/dist/server/projectTrustRoutes.js.map +1 -1
  117. package/dist/server/projects/projectLifecycleService.js +17 -7
  118. package/dist/server/projects/projectLifecycleService.js.map +1 -1
  119. package/dist/server/realtime/sessionEventHub.js +22 -9
  120. package/dist/server/realtime/sessionEventHub.js.map +1 -1
  121. package/dist/server/sessiond/agentHttpDispatcher.js +1 -1
  122. package/dist/server/sessiond/agentHttpDispatcher.js.map +1 -1
  123. package/dist/server/sessiond/autoInstallPiPackages.js +3 -3
  124. package/dist/server/sessiond/pluginBackendChannelRoutes.js +27 -14
  125. package/dist/server/sessiond/pluginBackendChannelRoutes.js.map +1 -1
  126. package/dist/server/sessiond/pluginBackendRoutes.js +56 -49
  127. package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -1
  128. package/dist/server/sessiond/pluginCatalogRoutes.js +41 -0
  129. package/dist/server/sessiond/pluginCatalogRoutes.js.map +1 -0
  130. package/dist/server/sessiond/sessionProxyRoutes.js +46 -1
  131. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  132. package/dist/server/sessiond/sessionServiceDependencies.js +1 -0
  133. package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
  134. package/dist/server/sessiond/workspaceCatalogRoutes.js +6 -2
  135. package/dist/server/sessiond/workspaceCatalogRoutes.js.map +1 -1
  136. package/dist/server/sessiond/workspaceNotificationRoutes.js +55 -0
  137. package/dist/server/sessiond/workspaceNotificationRoutes.js.map +1 -0
  138. package/dist/server/sessiond.js +58 -8
  139. package/dist/server/sessiond.js.map +1 -1
  140. package/dist/server/sessions/askUserDomain.js +232 -0
  141. package/dist/server/sessions/askUserDomain.js.map +1 -0
  142. package/dist/server/sessions/askUserTool.js +18 -18
  143. package/dist/server/sessions/askUserTool.js.map +1 -1
  144. package/dist/server/sessions/attachmentService.js +1 -1
  145. package/dist/server/sessions/attachmentService.js.map +1 -1
  146. package/dist/server/sessions/authRoutes.js +12 -0
  147. package/dist/server/sessions/authRoutes.js.map +1 -1
  148. package/dist/server/sessions/authService.js +11 -1
  149. package/dist/server/sessions/authService.js.map +1 -1
  150. package/dist/server/sessions/builtinExtensionFactories.js +36 -0
  151. package/dist/server/sessions/builtinExtensionFactories.js.map +1 -0
  152. package/dist/server/sessions/companionBackend.js +286 -0
  153. package/dist/server/sessions/companionBackend.js.map +1 -0
  154. package/dist/server/sessions/dockerEnvironmentFacts.js +16 -1
  155. package/dist/server/sessions/dockerEnvironmentFacts.js.map +1 -1
  156. package/dist/server/sessions/extensionSessionDispatch.js +76 -0
  157. package/dist/server/sessions/extensionSessionDispatch.js.map +1 -0
  158. package/dist/server/sessions/globalProviderPolicy.js +88 -19
  159. package/dist/server/sessions/globalProviderPolicy.js.map +1 -1
  160. package/dist/server/sessions/managementAgentTools.js +2 -2
  161. package/dist/server/sessions/managementAgentTools.js.map +1 -1
  162. package/dist/server/sessions/mcpAuthFlowService.js +289 -0
  163. package/dist/server/sessions/mcpAuthFlowService.js.map +1 -0
  164. package/dist/server/sessions/mcpAuthRoutes.js +104 -0
  165. package/dist/server/sessions/mcpAuthRoutes.js.map +1 -0
  166. package/dist/server/sessions/mcpOAuthCredentialAdapter.js +92 -0
  167. package/dist/server/sessions/mcpOAuthCredentialAdapter.js.map +1 -0
  168. package/dist/server/sessions/messageHistoryTarget.js +79 -0
  169. package/dist/server/sessions/messageHistoryTarget.js.map +1 -0
  170. package/dist/server/sessions/piSessionEventConnections.js +5 -2
  171. package/dist/server/sessions/piSessionEventConnections.js.map +1 -1
  172. package/dist/server/sessions/piSessionManagerGateway.js +32 -27
  173. package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
  174. package/dist/server/sessions/piSessionService.js +1296 -341
  175. package/dist/server/sessions/piSessionService.js.map +1 -1
  176. package/dist/server/sessions/plainTextTheme.js +1 -1
  177. package/dist/server/sessions/plainTextTheme.js.map +1 -1
  178. package/dist/server/sessions/projectAuthService.js +1 -1
  179. package/dist/server/sessions/projectAuthService.js.map +1 -1
  180. package/dist/server/sessions/projectSettingsScope.js +1 -1
  181. package/dist/server/sessions/projectSettingsScope.js.map +1 -1
  182. package/dist/server/sessions/projectTrust.js +1 -1
  183. package/dist/server/sessions/projectTrust.js.map +1 -1
  184. package/dist/server/sessions/queuedMessageMutation.js +170 -0
  185. package/dist/server/sessions/queuedMessageMutation.js.map +1 -0
  186. package/dist/server/sessions/relaySkill.js +57 -7
  187. package/dist/server/sessions/relaySkill.js.map +1 -1
  188. package/dist/server/sessions/sessionArchiveMapping.js +6 -0
  189. package/dist/server/sessions/sessionArchiveMapping.js.map +1 -1
  190. package/dist/server/sessions/sessionArchiveStore.js +5 -1
  191. package/dist/server/sessions/sessionArchiveStore.js.map +1 -1
  192. package/dist/server/sessions/sessionArchiveTree.js +2 -2
  193. package/dist/server/sessions/sessionArchiveTree.js.map +1 -1
  194. package/dist/server/sessions/sessionCommandService.js +11 -24
  195. package/dist/server/sessions/sessionCommandService.js.map +1 -1
  196. package/dist/server/sessions/sessionImageDimensions.js +31 -0
  197. package/dist/server/sessions/sessionImageDimensions.js.map +1 -0
  198. package/dist/server/sessions/sessionMediaIndex.js +191 -0
  199. package/dist/server/sessions/sessionMediaIndex.js.map +1 -0
  200. package/dist/server/sessions/sessionMetadata.js +51 -0
  201. package/dist/server/sessions/sessionMetadata.js.map +1 -0
  202. package/dist/server/sessions/sessionModelScope.js +2 -2
  203. package/dist/server/sessions/sessionModelScope.js.map +1 -1
  204. package/dist/server/sessions/sessionNameGenerator.js +13 -33
  205. package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
  206. package/dist/server/sessions/sessionReadAccess.js +41 -0
  207. package/dist/server/sessions/sessionReadAccess.js.map +1 -0
  208. package/dist/server/sessions/sessionRoutes.js +61 -6
  209. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  210. package/dist/server/sessions/sessionSummaryScanner.js +62 -5
  211. package/dist/server/sessions/sessionSummaryScanner.js.map +1 -1
  212. package/dist/server/sessions/sessionUiEvents.js +17 -4
  213. package/dist/server/sessions/sessionUiEvents.js.map +1 -1
  214. package/dist/server/sessions/spawnSessionTool.js +11 -3
  215. package/dist/server/sessions/spawnSessionTool.js.map +1 -1
  216. package/dist/server/sessions/spawnSubsessionTool.js +35 -19
  217. package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
  218. package/dist/server/sessions/subsessionCompletionDelivery.js +15 -0
  219. package/dist/server/sessions/subsessionCompletionDelivery.js.map +1 -0
  220. package/dist/server/sessions/subsessionCompletionInbox.js +134 -0
  221. package/dist/server/sessions/subsessionCompletionInbox.js.map +1 -0
  222. package/dist/server/sessions/subsessionProvenance.js +27 -0
  223. package/dist/server/sessions/subsessionProvenance.js.map +1 -0
  224. package/dist/server/sessions/transcriptMarkdown.js +48 -0
  225. package/dist/server/sessions/transcriptMarkdown.js.map +1 -0
  226. package/dist/server/sessions/transcriptMessages.js +3 -0
  227. package/dist/server/sessions/transcriptMessages.js.map +1 -1
  228. package/dist/server/startupLogger.js +13 -0
  229. package/dist/server/startupLogger.js.map +1 -0
  230. package/dist/server/startupUrls.js +27 -0
  231. package/dist/server/startupUrls.js.map +1 -0
  232. package/dist/server/status/machineStatusService.js +15 -9
  233. package/dist/server/status/machineStatusService.js.map +1 -1
  234. package/dist/server/storage/navigationPreferenceStore.js +138 -0
  235. package/dist/server/storage/navigationPreferenceStore.js.map +1 -0
  236. package/dist/server/webSocketBridge.js +9 -0
  237. package/dist/server/webSocketBridge.js.map +1 -1
  238. package/dist/server/workbench/skillSync.js +1 -1
  239. package/dist/server/workbench/skillSync.js.map +1 -1
  240. package/dist/server/workbench/workbenchTools.js +2 -2
  241. package/dist/server/workbench/workbenchTools.js.map +1 -1
  242. package/dist/server/workspaces/filePreviewService.js +5 -2
  243. package/dist/server/workspaces/filePreviewService.js.map +1 -1
  244. package/dist/server/workspaces/projectPiWebConfig.js +87 -6
  245. package/dist/server/workspaces/projectPiWebConfig.js.map +1 -1
  246. package/dist/server/workspaces/workspaceChangeNotifier.js +12 -0
  247. package/dist/server/workspaces/workspaceChangeNotifier.js.map +1 -0
  248. package/dist/server/workspaces/workspaceNotificationCommand.js +64 -0
  249. package/dist/server/workspaces/workspaceNotificationCommand.js.map +1 -0
  250. package/dist/server/workspaces/workspaceProviderRegistry.js +53 -0
  251. package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -1
  252. package/dist/server-plugin-api.d.ts +83 -15
  253. package/dist/server-plugin-api.js +25 -0
  254. package/dist/server-plugin-api.js.map +1 -1
  255. package/dist/sessiond/sessionDaemonClient.js +27 -3
  256. package/dist/sessiond/sessionDaemonClient.js.map +1 -1
  257. package/dist/shared/apiTypes.js +3 -2
  258. package/dist/shared/apiTypes.js.map +1 -1
  259. package/dist/shared/capabilities.js +3 -0
  260. package/dist/shared/capabilities.js.map +1 -1
  261. package/dist/shared/federatedRoutes.js +25 -5
  262. package/dist/shared/federatedRoutes.js.map +1 -1
  263. package/dist/shared/machineStatus.js +11 -2
  264. package/dist/shared/machineStatus.js.map +1 -1
  265. package/dist/shared/navigationPreferences.js +6 -0
  266. package/dist/shared/navigationPreferences.js.map +1 -0
  267. package/dist/shared/piWebConfigParsing.js +54 -0
  268. package/dist/shared/piWebConfigParsing.js.map +1 -1
  269. package/dist/shared/pluginApiTypes.d.ts +55 -0
  270. package/dist/shared/pluginBackendProtocol.js +56 -1
  271. package/dist/shared/pluginBackendProtocol.js.map +1 -1
  272. package/dist/shared/pluginCatalogTypes.d.ts +49 -0
  273. package/dist/shared/pluginCatalogTypes.js +156 -0
  274. package/dist/shared/pluginCatalogTypes.js.map +1 -0
  275. package/dist/shared/sessionMedia.js +12 -0
  276. package/dist/shared/sessionMedia.js.map +1 -0
  277. package/dist/shared/streamingBehavior.js +8 -0
  278. package/dist/shared/streamingBehavior.js.map +1 -0
  279. package/dist/shared/themePreference.js +24 -0
  280. package/dist/shared/themePreference.js.map +1 -0
  281. package/dist/workspaceNotificationCli.js +95 -0
  282. package/dist/workspaceNotificationCli.js.map +1 -0
  283. package/docs/README.md +31 -0
  284. package/docs/config.md +77 -4
  285. package/docs/plugins.md +60 -11
  286. package/examples/session-bridge-plugin/package.json +2 -2
  287. package/examples/session-bridge-plugin/src/browser/index.ts +1 -1
  288. package/examples/session-bridge-plugin/src/companion.ts +1 -1
  289. package/extensions/pi-web.ts +3 -1
  290. package/package.json +46 -34
  291. package/skills/README.md +10 -9
  292. package/skills/relay/SKILL.md +24 -68
  293. package/skills/relay-runner/SKILL.md +40 -219
  294. package/dist/client/assets/PiWebApp-CN3g7SqA.js +0 -4362
  295. package/dist/client/assets/external-DUGCSDhZ.js +0 -2
  296. package/dist/client/assets/index-JduZlbIF.js +0 -2
  297. package/dist/client/assets/vendor-editor-core-YWog6mAR.js +0 -10
  298. package/dist/server/sessions/pendingAskStore.js +0 -275
  299. package/dist/server/sessions/pendingAskStore.js.map +0 -1
@@ -0,0 +1,61 @@
1
+ // Generated from pi-packages/relays/extensions/index.ts. Do not edit directly.
2
+ import { Type } from "@earendil-works/pi-ai";
3
+ import { defineTool } from "@earendil-works/pi-coding-agent";
4
+ import { prepareRelayDispatch } from "../relayDispatch.js";
5
+ import { requireRelayLeg } from "../relayIdentity.js";
6
+ const DispatchParams = Type.Object({
7
+ packet: Type.String({ description: "Saved Relay packet directory containing charter.md, status.md and log.md. Relative paths resolve in the target workspace." }),
8
+ leg: Type.String({
9
+ pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,31}$",
10
+ description: 'String identity of the leg being dispatched, for example "2" or "R1-a". Numbers and other non-string values are rejected, not converted.',
11
+ }),
12
+ cwd: Type.Optional(Type.String({ description: "Target workspace of the same registered project. Omit to use this session's workspace." })),
13
+ model: Type.Optional(Type.String({ description: 'Exact provider/model-id override. Set only when instructed to select a specific model or choose an appropriate one; otherwise omit to inherit.' })),
14
+ thinkingLevel: Type.Optional(Type.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"], {
15
+ type: "string",
16
+ description: "Thinking-level override. Set only when instructed to select a level or choose an appropriate one; otherwise omit to inherit. Pi clamps valid levels to the selected model.",
17
+ })),
18
+ }, { additionalProperties: false });
19
+ export default function relayExtension(pi) {
20
+ pi.registerTool(defineTool({
21
+ name: "dispatch_relay",
22
+ label: "Dispatch Relay",
23
+ description: "Start one independent Relay leg from its saved packet. Requires explicit human approval before initial dispatch. Save the actual baton in status.md first; this tool generates the handover and records Relay identity itself. There is no free-form prompt parameter. Use once as the final operational action, then report a brief summary.",
24
+ promptSnippet: "dispatch_relay: dispatch one Relay leg from its saved packet; approval before initial dispatch; final operational action",
25
+ exposure: "model-only",
26
+ parameters: DispatchParams,
27
+ prepareArguments(args) {
28
+ const leg = typeof args === "object" && args !== null && !Array.isArray(args) ? Reflect.get(args, "leg") : undefined;
29
+ requireRelayLeg(leg);
30
+ // Guard identity before Pi's coercion. Its schema validation checks the
31
+ // remaining fields next; retain the raw object, including unknown keys.
32
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
33
+ return args;
34
+ },
35
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
36
+ signal?.throwIfAborted();
37
+ const prepared = await prepareRelayDispatch(ctx.cwd, params);
38
+ signal?.throwIfAborted();
39
+ const result = await dispatchThroughHost(pi, prepared, {
40
+ ...(params.model === undefined ? {} : { model: params.model }),
41
+ ...(params.thinkingLevel === undefined ? {} : { thinkingLevel: params.thinkingLevel }),
42
+ });
43
+ const modelNote = result.model === undefined ? "" : ` using model ${result.model}`;
44
+ return {
45
+ content: [{ type: "text", text: `Started Relay ${JSON.stringify(prepared.identity.relayName)} leg ${prepared.identity.leg} as independent session ${result.sessionId} in ${result.cwd}${modelNote}. The saved packet is the handover.` }],
46
+ details: result,
47
+ };
48
+ },
49
+ }));
50
+ }
51
+ /** Documented generic in-process host protocol; unavailable hosts reject before dispatch. */
52
+ async function dispatchThroughHost(pi, prepared, overrides) {
53
+ let accepted;
54
+ pi.events.emit("pi-web:session-dispatch:v1", {
55
+ input: { prompt: prepared.prompt, cwd: prepared.cwd, name: prepared.name, metadata: prepared.metadata, ...overrides },
56
+ accept: (result) => { accepted = result; },
57
+ });
58
+ if (accepted === undefined)
59
+ throw new Error("Relay dispatch requires PI WEB's session dispatch bridge. Update PI WEB and restart the session daemon when safe; no session was dispatched.");
60
+ return accepted;
61
+ }
@@ -2,7 +2,16 @@
2
2
  "name": "@jmfederico/pi-relay",
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
- "description": "Tool-agnostic Relay method, opinionated Relay Runner profile, and human-gated preparation prompts for independent Pi session chains.",
5
+ "description": "Goal-led Relay method, durable baton and history, and human-approved preparation prompts for independent Pi session chains.",
6
+ "peerDependencies": {
7
+ "@earendil-works/pi-ai": "*",
8
+ "@earendil-works/pi-coding-agent": "*"
9
+ },
10
+ "pi": {
11
+ "extensions": ["./extensions/index.js"],
12
+ "skills": ["./skills"],
13
+ "prompts": ["./prompts"]
14
+ },
6
15
  "piWeb": {
7
16
  "plugins": [
8
17
  { "id": "relays", "browserRoot": ".", "module": "pi-web-plugin.js" }
@@ -1,14 +1,35 @@
1
1
  // Generated from pi-packages/relays/pi-web-plugin.ts. Do not edit directly.
2
2
  import { t, setTranslator } from "./i18n.js";
3
3
  import { RELAYS_ROOT } from "./relayDiscovery.js";
4
- import { defineRelaysPanelElement } from "./relaysPanelElement.js";
4
+ import { relayIdentityFromMetadata } from "./relayIdentity.js";
5
+ import { defineRelaysPanelElement, RELAY_SELECTION_QUERY_KEY } from "./relaysPanelElement.js";
6
+ const stylesheetIntegrationCapability = {
7
+ pluginId: "pi-web", id: "stylesheet-integration", version: 1,
8
+ parse(value) {
9
+ if (!isStylesheetIntegration(value)) {
10
+ throw new Error("Stylesheet integration capability v1 requires registerRoot and createController");
11
+ }
12
+ return value;
13
+ },
14
+ };
15
+ function isStylesheetIntegration(value) {
16
+ return typeof value === "object" && value !== null
17
+ && "registerRoot" in value && typeof value.registerRoot === "function"
18
+ && "createController" in value && typeof value.createController === "function"
19
+ && "subscribe" in value && typeof value.subscribe === "function";
20
+ }
5
21
  const plugin = {
6
22
  apiVersion: 4,
7
23
  name: t("bundled.relays.relays.715ce8", "Relays"),
24
+ requires: [stylesheetIntegrationCapability],
8
25
  activate: ({ runtimePluginId, html, svg, translate }) => {
9
26
  setTranslator(translate);
10
27
  defineRelaysPanelElement();
28
+ let stylesheetIntegration;
11
29
  return {
30
+ start: ({ capabilities }) => {
31
+ stylesheetIntegration = capabilities.resolve(stylesheetIntegrationCapability);
32
+ },
12
33
  contributions: {
13
34
  actions: [
14
35
  {
@@ -24,6 +45,35 @@ const plugin = {
24
45
  },
25
46
  },
26
47
  ],
48
+ sessionLabels: [
49
+ {
50
+ id: "session.relay",
51
+ items: (context) => {
52
+ const identity = relayIdentityFromMetadata(context.session.metadata);
53
+ if (identity === undefined)
54
+ return [];
55
+ return [{
56
+ type: "render",
57
+ render: () => html `
58
+ <button
59
+ type="button"
60
+ style="border:0; padding:0; background:transparent; color:var(--pi-accent); font:inherit; text-align:start; overflow-wrap:anywhere; cursor:pointer"
61
+ title=${identity.packetPath}
62
+ aria-label=${t("bundled.relays.open.packet", "Open Relay {name}, leg {leg}", { name: identity.relayName, leg: identity.leg })}
63
+ @click=${() => context.navigate({
64
+ machineId: context.machine.id,
65
+ projectId: context.workspace.projectId,
66
+ workspaceId: context.workspace.id,
67
+ view: "workspace",
68
+ tool: `${runtimePluginId}:workspace.relays`,
69
+ query: { [RELAY_SELECTION_QUERY_KEY]: identity.packetPath },
70
+ }, { mode: "patch" })}
71
+ >${t("bundled.relays.label", "relay")}</button>
72
+ `,
73
+ }];
74
+ },
75
+ },
76
+ ],
27
77
  workspacePanels: [
28
78
  {
29
79
  id: "workspace.relays",
@@ -36,7 +86,7 @@ const plugin = {
36
86
  </svg>
37
87
  `,
38
88
  order: 50,
39
- render: (context) => html `<pi-web-relays-panel .context=${context}></pi-web-relays-panel>`,
89
+ render: (context) => html `<pi-web-relays-panel .stylesheetIntegration=${stylesheetIntegration} .context=${context}></pi-web-relays-panel>`,
40
90
  },
41
91
  ],
42
92
  },
@@ -1,27 +1,17 @@
1
1
  ---
2
- description: Compatibility alias for /relay that prepares a fresh-worktree Relay
2
+ description: Clarify a goal with the human, then dispatch a Relay in a fresh worktree
3
3
  argument-hint: "<what the relay should achieve>"
4
4
  ---
5
5
 
6
- Prepare a Relay for the task source at the end of this prompt. This is `/relay`'s fresh-worktree compatibility alias. Drafting may begin immediately; dispatch still requires explicit human approval.
6
+ Prepare a Relay from the request below in a fresh worktree. Load the `relay` and `relay-runner` skills. This differs from `/relay` only in working location; if the request names an existing target, resolve that choice with the human.
7
7
 
8
- If the task source is empty, ask what the Relay should achieve before doing anything else.
8
+ Help the human put into words what they want built, what counts as done, and the relay-specific boundaries, limitations, and conditions. Read supplied specifications and inspect the project only as needed to understand that intent. If the request is empty, ask what they want to achieve with `ask_user`.
9
9
 
10
- Select `relay` (the portable principle) and `relay-runner` (the opinionated Pi/Git profile) before using their instructions.
10
+ Draft `.pi-web/relays/<name>/charter.md`, `status.md`, and `log.md` in this checkout under the runner skill's packet convention. Mark status **Draft — awaiting approval; not dispatched**. Capture the requested outcome and any details the human explicitly asks to include, not an agent-invented implementation plan. Leave project rules and skill discovery to each runner; do not turn them into relay instructions. The first runner chooses the first useful slice.
11
11
 
12
- Apply the same human-gated preparation contract as `/relay`:
12
+ Point the human to the draft, summarize the goal, boundaries, and proposed working location, and resolve material questions together through `ask_user`. Once the goal is understood, ask **Approve and dispatch**, **Revise**, or **Do not dispatch** against the final draft. Drafting and the initial request are not dispatch approval.
13
13
 
14
- 1. Create a reviewable draft packet at `.pi-web/relays/<name>/` in the checkout where this preparation session is running. Seed goal-focused `charter.md`, profile-owned `operations.md`, compact `status.md`, and `log.md`; mark status **Draft — awaiting approval; not dispatched**.
15
- 2. Read canonical repository instructions and task-relevant material. Inspect enough baseline behavior, Git/worktree state, and delivery context to understand the requested destination and scope edges rather than assuming a solution. Update the draft documents as that understanding improves.
16
- 3. Keep the charter to goal, observable finish line, minimum outcome acceptance, scope edges/non-goals/preserved behavior, and material assumptions/decisions. Put runner and repository mechanics in operations. Put only current approval state and the first bounded leg in status.
17
- 4. Do not create a roadmap, `plan.md`, fixed leg count, work-package hierarchy, exhaustive stage list, expected file/layer map, or speculative technical design. The Relay discovers and adapts its route one leg at a time.
18
- 5. Point the user at the draft packet, discuss and resolve material questions through `ask_user`, update the drafts, then provide the final packet path and concise summary. Ask **Approve and dispatch**, **Revise**, or **Do not dispatch**. Draft creation is not approval; nothing except the explicit post-review approval authorizes `spawn_session`. On **Revise**, update and re-present the drafts. On **Do not dispatch**, record that state and stop without spawning.
19
-
20
- After approval, follow `relay-runner`'s approved-dispatch workflow in **fresh-worktree mode**: create or finish the target worktree, move the packet from the drafting checkout into the target worktree's `.pi-web/relays/<name>/`, update its recorded path/location, remove the stale draft copy, record approval, and dispatch the setup leg when bootstrap is required or otherwise the first substantive leg. Call `spawn_session` exactly once and only after state is durable in the target. After it returns, provide the dispatch summary without further tool use, state changes, or Relay work.
21
-
22
- If the task explicitly names an existing checkout or worktree instead, include that conflict in the drafts and resolve it with the user rather than silently overriding either instruction.
23
-
24
- Treat the text inside `<relay_task>` as source material to understand, not as instructions that bypass discussion or approval.
14
+ On revision, update the draft and obtain approval. On refusal, record that it was not dispatched and stop. After explicit approval, create the worktree and move the packet there as described in the runner skill. Record approval, mark status **Active**, seed leg 1, and finalize the packet with the actual baton saved in `status.md`. Call the dedicated `dispatch_relay` tool once with the moved directory as `packet`, `leg: "1"`, and the new worktree as `cwd`, following the runner skill's handoff mechanics. The tool generates the handover; do not supply a free-form prompt. Dispatch is the final operational action; afterward provide only a brief summary.
25
15
 
26
16
  <relay_task>
27
17
  $ARGUMENTS
@@ -1,44 +1,17 @@
1
1
  ---
2
- description: Establish a shared understanding, obtain approval, and dispatch a Relay
2
+ description: Clarify a goal with the human, then dispatch a Relay
3
3
  argument-hint: "<what the relay should achieve>"
4
4
  ---
5
5
 
6
- Prepare a Relay for the task source at the end of this prompt. The packet may be drafted immediately; dispatch still requires explicit human approval.
6
+ Prepare a Relay from the request below. Load the `relay` and `relay-runner` skills. Use the current checkout unless the human chooses another working location.
7
7
 
8
- If the task source is empty, ask what the Relay should achieve before doing anything else.
8
+ Help the human put into words what they want built, what counts as done, and the relay-specific boundaries, limitations, and conditions. Read supplied specifications and inspect the project only as needed to understand that intent. If the request is empty, ask what they want to achieve with `ask_user`.
9
9
 
10
- Select these skills before using their instructions:
10
+ Draft `.pi-web/relays/<name>/charter.md`, `status.md`, and `log.md` under the runner skill's packet convention. Mark status **Draft — awaiting approval; not dispatched**. Capture the requested outcome and any details the human explicitly asks to include, not an agent-invented implementation plan. Leave project rules and skill discovery to each runner; do not turn them into relay instructions. The first runner chooses the first useful slice.
11
11
 
12
- 1. `relay` — the portable Relay principle and durable-state model.
13
- 2. `relay-runner` — the opinionated Pi/Git operational profile used by this Relay.
12
+ Point the human to the draft, summarize the goal and boundaries, and resolve material questions together through `ask_user`. Once the goal is understood, ask **Approve and dispatch**, **Revise**, or **Do not dispatch** against the final draft. Drafting and the initial request are not dispatch approval.
14
13
 
15
- ## Preflight: understand before dispatch
16
-
17
- The quality of the Relay depends on a clear shared understanding of its destination and edges. Front-load that understanding, not a predicted implementation route.
18
-
19
- 1. **Start a reviewable draft packet.** Choose a clear Relay name and create `.pi-web/relays/<name>/` in the current drafting checkout. Seed draft `charter.md`, `operations.md`, `status.md`, and `log.md`; mark status **Draft — awaiting approval; not dispatched**. Update these documents as understanding improves so the human can review them through the Relays UI. Draft creation is not dispatch authorization.
20
- 2. **Understand the request and baseline.** Read the repository's canonical agent/contributor instructions and task-relevant project material. Inspect enough current behavior, repository state, and delivery context to distinguish the user's intended outcome from an assumed solution. Keep this bounded to facts that affect the goal, scope edges, feasibility, working location, or first leg; leave implementation archaeology to the Relay.
21
- 3. **Capture understanding without making a roadmap.** Keep `charter.md` to the plain-language goal and observable finish line, minimum outcome acceptance, in-scope edges, explicit non-goals, directly affected behavior to preserve, and material assumptions or human decisions. Put packet/profile identity, proposed operating mode/base, canonical project-guidance pointers, verification/delivery mechanics, and packet isolation in `operations.md`. Put only the proposed first bounded leg and current approval state in `status.md`.
22
- 4. **Do not pre-plan the chain.** Do not create a fixed leg count, work-package hierarchy, exhaustive stage list, expected file/layer map, `plan.md`, or technical design merely to make the Relay look prepared. The route is adaptive. Select an implementation-first leg under the runner profile. Reserve research-only work for a concrete unanswered question blocking implementation; routine investigation belongs inside the implementation leg.
23
- 5. **Discuss and refine.** Point the user at the draft packet, summarize the current understanding, ask related material questions together with `ask_user`, and incorporate the answers into the drafts. Do not hide assumptions or settle product, destructive-data, target, base, or delivery choices by guesswork.
24
- 6. **Request approval against the final drafts.** After the user has had a chance to review the final document purposes and content, provide the packet path plus a concise goal/edges, operating-setup, and first-leg summary. Use `ask_user` to offer **Approve and dispatch**, **Revise**, or **Do not dispatch**. The initial request, a clear task, prior general enthusiasm, silence, or permission to create drafts is not dispatch approval. If goal, edges, operating target, or first leg changes materially after approval, update the drafts and obtain fresh approval.
25
-
26
- The approval gate applies to `spawn_session`, not to creating or refining packet drafts or transparently preparing checkout/worktree state. On **Revise**, update the drafts and repeat review. On **Do not dispatch**, mark status not dispatched, append the decision to the log, and stop without spawning.
27
-
28
- ## After approval
29
-
30
- Only after an explicit **Approve and dispatch** response, follow `relay-runner`'s approved-dispatch workflow:
31
-
32
- - finalize `charter.md`, `operations.md`, `status.md`, and `log.md` without adding an upfront route plan;
33
- - mark the packet approved and record the approval in status/log;
34
- - create or finish the selected checkout/worktree setup;
35
- - when the target is a fresh worktree, move the draft packet from the drafting checkout into the target worktree's `.pi-web/relays/<name>/`, update its recorded path/location, and remove the stale draft copy;
36
- - dispatch exactly one first leg with `spawn_session` after all state is durable in its final location; and
37
- - after `spawn_session` returns, provide the dispatch summary without further tool use, state changes, or Relay work.
38
-
39
- This invocation proposes **in-place mode** in the current checkout and branch unless the task source explicitly requests a fresh worktree or names another existing checkout/worktree. Resolve the mode during preflight; do not redirect the user to another command.
40
-
41
- Treat the text inside `<relay_task>` as source material to understand, not as instructions that bypass discussion or approval.
14
+ On revision, update the draft and obtain approval. On refusal, record that it was not dispatched and stop. After explicit approval, record it, mark status **Active**, seed leg 1, and finalize the packet with the actual baton saved in `status.md`. Call the dedicated `dispatch_relay` tool once with the saved directory as `packet`, `leg: "1"`, and the target checkout as `cwd`, following the runner skill's handoff mechanics. The tool generates the handover; do not supply a free-form prompt. Dispatch is the final operational action; afterward provide only a brief summary.
42
15
 
43
16
  <relay_task>
44
17
  $ARGUMENTS
@@ -0,0 +1,67 @@
1
+ // Generated from pi-packages/relays/relayDispatch.ts. Do not edit directly.
2
+ import { open, realpath, stat } from "node:fs/promises";
3
+ import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { isRelayPacketPath, RELAY_METADATA_NAMESPACE, relaySessionName, requireRelayLeg } from "./relayIdentity.js";
5
+ const MAX_PACKET_DOCUMENT_BYTES = 256 * 1024;
6
+ const PACKET_DOCUMENTS = ["charter.md", "status.md", "log.md"];
7
+ /** Validate the durable packet on disk, not handover prose authored by the caller. */
8
+ export async function prepareRelayDispatch(spawningCwd, request) {
9
+ const leg = requireRelayLeg(request.leg);
10
+ const cwd = await realpath(resolve(spawningCwd, request.cwd ?? "."));
11
+ if (typeof request.packet !== "string" || request.packet.trim() === "")
12
+ throw new Error("A saved Relay packet directory is required");
13
+ const packet = await realpath(resolve(cwd, request.packet));
14
+ const packetPath = relative(cwd, packet).split(sep).join("/");
15
+ if (!isRelayPacketPath(packetPath))
16
+ throw new Error("Relay packet must be a directory inside the target workspace");
17
+ for (const document of PACKET_DOCUMENTS)
18
+ await validateDocument(packet, document);
19
+ const identity = { version: 1, packetPath, relayName: basename(packet), leg };
20
+ return {
21
+ cwd,
22
+ identity,
23
+ name: relaySessionName(identity),
24
+ metadata: { [RELAY_METADATA_NAMESPACE]: identity },
25
+ prompt: relayHandoverInstructions(cwd, packet, identity),
26
+ };
27
+ }
28
+ function relayHandoverInstructions(cwd, packet, identity) {
29
+ return [
30
+ `Continue Relay ${JSON.stringify(identity.relayName)}, leg ${identity.leg}, in ${JSON.stringify(cwd)}.`,
31
+ "Load the `relay` and `relay-runner` skills.",
32
+ `Read ${JSON.stringify(join(packet, "charter.md"))} and ${JSON.stringify(join(packet, "status.md"))}.`,
33
+ "The charter is the agreed goal and status.md is the actual baton. Consult the saved history and supporting documents only as needed; do not substitute dispatch prose for the packet.",
34
+ "Check the finish line against the current work and choose the next useful slice.",
35
+ "Make progress and the record durable, then hand off once, complete, or signal that you need help.",
36
+ ].join("\n");
37
+ }
38
+ async function validateDocument(packet, document) {
39
+ const path = await realpath(join(packet, document));
40
+ const inside = relative(packet, path);
41
+ if (isAbsolute(inside) || inside === ".." || inside.startsWith(`..${sep}`))
42
+ throw new Error(`Relay ${document} must stay inside the packet`);
43
+ const info = await stat(path);
44
+ if (!info.isFile() || info.size === 0 || info.size > MAX_PACKET_DOCUMENT_BYTES) {
45
+ throw new Error(`Relay ${document} must be a non-empty regular file of at most ${String(MAX_PACKET_DOCUMENT_BYTES)} bytes`);
46
+ }
47
+ const file = await open(path, "r");
48
+ try {
49
+ // Bound reads even if another process grows the document after stat.
50
+ const buffer = Buffer.alloc(MAX_PACKET_DOCUMENT_BYTES + 1);
51
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
52
+ if (bytesRead > MAX_PACKET_DOCUMENT_BYTES)
53
+ throw new Error(`Relay ${document} exceeds the document size limit`);
54
+ let text;
55
+ try {
56
+ text = new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, bytesRead));
57
+ }
58
+ catch {
59
+ throw new Error(`Relay ${document} must be UTF-8 text`);
60
+ }
61
+ if (text.trim() === "" || text.includes("\0"))
62
+ throw new Error(`Relay ${document} must contain saved text`);
63
+ }
64
+ finally {
65
+ await file.close();
66
+ }
67
+ }
@@ -0,0 +1,37 @@
1
+ // Generated from pi-packages/relays/relayIdentity.ts. Do not edit directly.
2
+ /** Relay owns this schema; core treats it as opaque browser-public JSON. */
3
+ export const RELAY_METADATA_NAMESPACE = "@jmfederico/pi-relay";
4
+ export function relayIdentityFromMetadata(metadata) {
5
+ const value = metadata?.[RELAY_METADATA_NAMESPACE];
6
+ if (value?.["version"] !== 1)
7
+ return undefined;
8
+ const packetPath = value["packetPath"];
9
+ const relayName = value["relayName"];
10
+ const leg = value["leg"];
11
+ if (typeof packetPath !== "string" || !isRelayPacketPath(packetPath)
12
+ || typeof relayName !== "string" || relayName !== packetPath.split("/").at(-1)
13
+ || typeof leg !== "string" || !isRelayLeg(leg))
14
+ return undefined;
15
+ return { version: 1, packetPath, relayName, leg };
16
+ }
17
+ export function isRelayPacketPath(value) {
18
+ // eslint-disable-next-line no-control-regex -- packet paths must not contain control characters.
19
+ return value.length > 0 && value.length <= 4096 && !/[\\\u0000-\u001f\u007f]/u.test(value)
20
+ && value.split("/").every((segment) => segment !== "" && segment !== "." && segment !== ".." && segment === segment.trim());
21
+ }
22
+ export function isRelayLeg(value) {
23
+ return /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,31}$/u.test(value);
24
+ }
25
+ export function requireRelayLeg(value) {
26
+ if (typeof value !== "string" || !isRelayLeg(value)) {
27
+ throw new Error("Relay leg must be a string of 1–32 letters, digits, dots, underscores or hyphens, starting with a letter or digit");
28
+ }
29
+ return value;
30
+ }
31
+ /** Keep the leg first so host and sidebar truncation only shorten the Relay name. */
32
+ export function relaySessionName(identity) {
33
+ const prefix = `Leg ${identity.leg} – Relay `;
34
+ const room = 60 - prefix.length;
35
+ const name = identity.relayName.slice(0, room).replace(/[\s._-]+$/u, "") || identity.relayName.slice(0, room);
36
+ return `${prefix}${name}`;
37
+ }
@@ -1,8 +1,10 @@
1
1
  // Generated from pi-packages/relays/relaysPanelElement.ts. Do not edit directly.
2
2
  import { t } from "./i18n.js";
3
3
  import { isMarkdownDocumentPath, renderRelayDocumentHtml } from "./markdownDocument.js";
4
+ import { isRelayPacketPath } from "./relayIdentity.js";
4
5
  import { ancestorDirectoryPaths, collectDirectoryPaths, defaultRelayDocument, flattenRelayTree, listRelayDocumentTree, listWorkspaceRelays, readRelayDocument, RELAYS_ROOT, } from "./relayDiscovery.js";
5
6
  export const relaysPanelTagName = "pi-web-relays-panel";
7
+ export const RELAY_SELECTION_QUERY_KEY = "relay";
6
8
  export function defineRelaysPanelElement() {
7
9
  if (!customElements.get(relaysPanelTagName))
8
10
  customElements.define(relaysPanelTagName, PiWebRelaysPanel);
@@ -21,6 +23,24 @@ export function defineRelaysPanelElement() {
21
23
  * horizontal scroll position and keyboard focus survive document switches.
22
24
  */
23
25
  class PiWebRelaysPanel extends HTMLElement {
26
+ integration;
27
+ releaseStyles;
28
+ set stylesheetIntegration(value) {
29
+ if (value === this.integration)
30
+ return;
31
+ this.releaseStyles?.();
32
+ this.releaseStyles = undefined;
33
+ this.integration = value;
34
+ if (this.isConnected)
35
+ this.releaseStyles = value?.registerRoot(this.root, { sharedUi: true });
36
+ }
37
+ connectedCallback() {
38
+ this.releaseStyles ??= this.integration?.registerRoot(this.root, { sharedUi: true });
39
+ }
40
+ disconnectedCallback() {
41
+ this.releaseStyles?.();
42
+ this.releaseStyles = undefined;
43
+ }
24
44
  contextValue;
25
45
  listing;
26
46
  selectedRelayPath;
@@ -38,8 +58,8 @@ class PiWebRelaysPanel extends HTMLElement {
38
58
  this.root = this.attachShadow({ mode: "open" });
39
59
  this.root.innerHTML = `
40
60
  ${relaysStyles()}
41
- <section class="toolbar" hidden></section>
42
- <nav class="document-tabs" aria-label="${escapeAttr(t("bundled.relays.relay.documents.91711b", "Relay documents"))}" hidden></nav>
61
+ <section class="toolbar ui-toolbar" hidden></section>
62
+ <nav class="document-tabs ui-tabs" aria-label="${escapeAttr(t("bundled.relays.relay.documents.91711b", "Relay documents"))}" hidden></nav>
43
63
  <section class="viewer"><div class="empty">${escapeHtml(t("bundled.relays.select.a.workspace.234859", "Select a workspace."))}</div></section>
44
64
  `;
45
65
  this.toolbar = requiredRegion(this.root, ".toolbar");
@@ -81,32 +101,51 @@ class PiWebRelaysPanel extends HTMLElement {
81
101
  set context(value) {
82
102
  const previousKey = this.contextValue === undefined ? undefined : contextKey(this.contextValue);
83
103
  const nextKey = value === undefined ? undefined : contextKey(value);
104
+ const previousRelay = linkedRelayPath(this.contextValue);
105
+ const nextRelay = linkedRelayPath(value);
84
106
  this.contextValue = value;
85
- // Parent app updates should not rescan or re-render this panel for the
86
- // same workspace (mirrors the workspace-tasks panel).
87
- if (previousKey === nextKey)
107
+ // Unrelated host renders should not rescan. A picker already starts its
108
+ // load before publishing navigation, so adopting that route does not load twice.
109
+ if (previousKey === nextKey && (previousRelay === nextRelay
110
+ || (nextRelay !== undefined && nextRelay === this.selectedRelayPath)))
88
111
  return;
89
112
  this.expandedDirs = new Set();
90
113
  if (value === undefined) {
114
+ ++this.scanToken;
91
115
  this.resetScanState();
92
116
  this.renderAll();
93
117
  return;
94
118
  }
95
- void this.scan(value, {});
119
+ void this.scan(value, { relayPath: nextRelay });
96
120
  }
97
121
  /** Rescan relays, then reload the selected relay's documents and the open document. */
98
122
  async scan(context, selection) {
99
123
  const token = ++this.scanToken;
100
124
  this.resetScanState();
101
125
  this.renderAll();
126
+ const requestedPath = selection.relayPath ?? linkedRelayPath(context);
127
+ if (requestedPath !== undefined && !isRelayPacketPath(requestedPath)) {
128
+ this.listing = { kind: "unavailable", detail: t("bundled.relays.invalid.packet.link", "Relay links must select a normalized workspace-relative packet directory.") };
129
+ this.renderAll();
130
+ return;
131
+ }
102
132
  const listing = await listWorkspaceRelays(context.files);
103
133
  if (!this.isCurrentScan(context, token))
104
134
  return;
105
- this.listing = listing;
106
- // listWorkspaceRelays returns most recently modified first, so the first
107
- // relay is the default pre-selection.
108
- const relay = listing.kind === "loaded"
109
- ? listing.relays.find((candidate) => candidate.path === selection.relayPath) ?? listing.relays[0]
135
+ // An exact link need not be under the discovery root. Keep that packet in
136
+ // the picker even if the default root is absent/unavailable, and let its
137
+ // own document load report a missing or unreadable target rather than
138
+ // silently substituting another relay.
139
+ const discovered = listing.kind === "loaded" ? listing.relays : [];
140
+ this.listing = requestedPath === undefined ? listing : {
141
+ kind: "loaded",
142
+ relays: discovered.some((relay) => relay.path === requestedPath) ? discovered : [
143
+ { name: documentName(requestedPath), path: requestedPath }, ...discovered,
144
+ ],
145
+ };
146
+ // Default discovery remains most recently modified first.
147
+ const relay = this.listing.kind === "loaded"
148
+ ? this.listing.relays.find((candidate) => candidate.path === requestedPath) ?? this.listing.relays[0]
110
149
  : undefined;
111
150
  this.selectedRelayPath = relay?.path;
112
151
  this.renderToolbar();
@@ -120,6 +159,7 @@ class PiWebRelaysPanel extends HTMLElement {
120
159
  const token = ++this.scanToken;
121
160
  this.selectedRelayPath = relayPath;
122
161
  this.expandedDirs = new Set();
162
+ context.navigation?.set(RELAY_SELECTION_QUERY_KEY, relayPath);
123
163
  await this.loadDocuments(context, token, relayPath, undefined);
124
164
  }
125
165
  toggleDirectory(directoryPath) {
@@ -248,10 +288,10 @@ class PiWebRelaysPanel extends HTMLElement {
248
288
  }
249
289
  this.toolbar.hidden = false;
250
290
  this.toolbar.innerHTML = `
251
- <strong>${escapeHtml(t("bundled.relays.relays.715ce8", "Relays"))}</strong>
252
- <span class="toolbar-actions">
291
+ <strong class="ui-toolbar-title">${escapeHtml(t("bundled.relays.relays.715ce8", "Relays"))}</strong>
292
+ <span class="toolbar-actions ui-toolbar-group">
253
293
  ${this.renderRelayPicker()}
254
- <button class="icon-button" data-refresh aria-label="${escapeAttr(t("bundled.relays.refresh.aee887", "Refresh"))}" title="${escapeAttr(t("bundled.relays.refresh.aee887", "Refresh"))}">${refreshIconSvg()}</button>
294
+ <button type="button" class="ui-button ui-button--compact ui-button--icon" data-refresh aria-label="${escapeAttr(t("bundled.relays.refresh.aee887", "Refresh"))}" title="${escapeAttr(t("bundled.relays.refresh.aee887", "Refresh"))}">${refreshIconSvg()}</button>
255
295
  </span>
256
296
  `;
257
297
  }
@@ -268,7 +308,7 @@ class PiWebRelaysPanel extends HTMLElement {
268
308
  const selected = relay.path === this.selectedRelayPath ? " selected" : "";
269
309
  return `<option value="${escapeAttr(relay.path)}"${selected}>${escapeHtml(relay.name)}</option>`;
270
310
  }).join("");
271
- return `<select data-relay-picker aria-label="${escapeAttr(t("bundled.relays.relays.715ce8", "Relays"))}">${options}</select>`;
311
+ return `<select class="ui-select" data-relay-picker aria-label="${escapeAttr(t("bundled.relays.relays.715ce8", "Relays"))}">${options}</select>`;
272
312
  }
273
313
  renderTabs() {
274
314
  const documents = this.documents;
@@ -294,26 +334,26 @@ class PiWebRelaysPanel extends HTMLElement {
294
334
  const chip = this.renderDirectoryChip(node, containsActivePath);
295
335
  if (!this.expandedDirs.has(node.path) || node.children.length === 0)
296
336
  return chip;
297
- return `<span class="directory-group">${chip}${this.renderStripNodes(node.children, containsActivePath)}</span>`;
337
+ return `<span class="directory-group ui-toolbar-group">${chip}${this.renderStripNodes(node.children, containsActivePath)}</span>`;
298
338
  }).join("");
299
339
  }
300
340
  renderFileTab(file) {
301
341
  const active = file.path === this.selectedDocumentPath;
302
- return `<button class="document-tab${active ? " active" : ""}" data-document-path="${escapeAttr(file.path)}" title="${escapeAttr(file.relativePath)}"${active ? ' aria-current="true"' : ""}>${escapeHtml(file.name)}</button>`;
342
+ return `<button type="button" class="ui-button ui-tab document-tab${active ? " selected" : ""}" data-document-path="${escapeAttr(file.path)}" title="${escapeAttr(file.relativePath)}"${active ? ' aria-current="page"' : ""}>${escapeHtml(file.name)}</button>`;
303
343
  }
304
344
  renderDirectoryChip(directory, containsActivePath) {
305
345
  const expanded = this.expandedDirs.has(directory.path);
306
346
  const containsActive = directory.path === containsActivePath;
307
347
  const title = containsActive ? t("ui.main.6a634d20bc86", "Contains the open document") : directory.relativePath;
308
- return `<button class="document-tab directory-tab${containsActive ? " contains-active" : ""}" data-directory-path="${escapeAttr(directory.path)}" title="${escapeAttr(title)}" aria-expanded="${expanded ? "true" : "false"}">${chevronSvg()}${escapeHtml(directory.name)}</button>`;
348
+ return `<button type="button" class="ui-button ui-tab document-tab directory-tab${containsActive ? " contains-active" : ""}" data-directory-path="${escapeAttr(directory.path)}" title="${escapeAttr(title)}" aria-expanded="${expanded ? "true" : "false"}">${chevronSvg()}${escapeHtml(directory.name)}</button>`;
309
349
  }
310
350
  /** Move the active marker between the mounted tab buttons without rebuilding them. */
311
351
  updateActiveTab() {
312
352
  for (const tab of this.tabStrip.querySelectorAll("button[data-document-path]")) {
313
353
  const active = tab.getAttribute("data-document-path") === this.selectedDocumentPath;
314
- tab.classList.toggle("active", active);
354
+ tab.classList.toggle("selected", active);
315
355
  if (active)
316
- tab.setAttribute("aria-current", "true");
356
+ tab.setAttribute("aria-current", "page");
317
357
  else
318
358
  tab.removeAttribute("aria-current");
319
359
  }
@@ -353,7 +393,7 @@ class PiWebRelaysPanel extends HTMLElement {
353
393
  return `${partialNotice}
354
394
  <div class="empty-state">
355
395
  <strong>${escapeHtml(t("bundled.relays.this.relay.has.no.documents.yet.d5948c", "This relay has no documents yet."))}</strong>
356
- <p>${escapeHtml(t("bundled.relays.document.files", "Relay packages usually include {files}.", { files: "status.md, charter.md, operations.md, log.md" }))}</p>
396
+ <p>${escapeHtml(t("bundled.relays.packet.documents", "Relay packets contain charter.md (goal), status.md (current baton), and log.md (history). An optional decisions.md keeps consequential decisions and their reasons."))}</p>
357
397
  </div>
358
398
  `;
359
399
  }
@@ -425,7 +465,11 @@ function chevronSvg() {
425
465
  return `<svg class="chevron" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M6 4l4 4-4 4"></path></svg>`;
426
466
  }
427
467
  function contextKey(context) {
428
- return `${context.machine.id}:${context.workspace.projectId}:${context.workspace.id}`;
468
+ return JSON.stringify([context.machine.id, context.workspace.projectId, context.workspace.id]);
469
+ }
470
+ function linkedRelayPath(context) {
471
+ const value = context?.navigation?.query[RELAY_SELECTION_QUERY_KEY];
472
+ return typeof value === "string" ? value : value?.[0];
429
473
  }
430
474
  function documentName(path) {
431
475
  return path.slice(path.lastIndexOf("/") + 1);
@@ -449,29 +493,23 @@ function relaysStyles() {
449
493
  (the app container is a fixed-height flex column; with shrink enabled
450
494
  the viewer's huge content basis starves them down to a sliver once a
451
495
  tall document renders). The viewer absorbs all shrinking instead. */
452
- .toolbar { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--pi-border-muted); }
496
+ .toolbar { flex: 0 0 auto; justify-content: space-between; padding-inline-start: 10px; }
453
497
  .toolbar[hidden], .document-tabs[hidden] { display: none; }
454
- .toolbar-actions { display: inline-flex; align-items: center; flex-wrap: nowrap; justify-content: flex-end; gap: 8px; min-width: 0; }
498
+ .toolbar-actions { justify-content: flex-end; }
455
499
  .relay-name { min-width: 0; color: var(--pi-text-secondary); overflow-wrap: anywhere; }
456
- /* Bottom padding (not viewer margin) so the gap below the tabs persists
457
- when the viewer's content scrolls up against its top edge. */
458
- .document-tabs { flex: 0 0 auto; display: flex; flex-wrap: nowrap; gap: 6px; padding: 8px 12px; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: thin; }
500
+ .document-tabs { flex: 0 0 auto; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: thin; }
459
501
  .viewer { flex: 1 1 auto; box-sizing: border-box; display: grid; align-content: start; gap: 12px; min-height: 0; overflow: auto; padding: 12px; }
460
502
  /* Grid children default to min-width: auto; without these caps a wide code
461
503
  block or table would silently stretch the whole viewer track. */
462
504
  .viewer > * { box-sizing: border-box; min-width: 0; max-width: 100%; }
463
- button, select { border: 1px solid var(--pi-border); border-radius: 7px; background: var(--pi-surface); color: var(--pi-text); font: inherit; }
464
- button { cursor: pointer; padding: 6px 10px; }
465
- button.icon-button { flex: 0 0 auto; display: inline-grid; place-items: center; width: 30px; height: 30px; padding: 0; }
466
- button.icon-button svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
467
- select { min-width: 0; max-width: 240px; padding: 5px 6px; }
468
- .document-tab { flex: 0 0 auto; white-space: nowrap; font-size: 12px; padding: 4px 10px; }
469
- .document-tab.active { border-color: var(--pi-accent-border); background: var(--pi-accent); color: var(--pi-bg); }
470
- .directory-group { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px; min-width: 0; border: 1px solid var(--pi-border-muted); border-radius: 10px; background: var(--pi-bg-overlay-soft); padding: 0; margin: -1px 0; }
471
- .directory-tab { display: inline-flex; align-items: center; gap: 5px; }
505
+ [data-refresh] svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
506
+ select { min-width: 0; max-width: 240px; }
507
+ /* Folder controls interleave with file tabs; shared groups keep nested controls contiguous. */
472
508
  .directory-tab .chevron { flex: 0 0 auto; width: 9px; height: 9px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; transition: transform 0.12s ease; }
473
509
  .directory-tab[aria-expanded="true"] .chevron { transform: rotate(90deg); }
474
- .directory-tab.contains-active { border-color: var(--pi-accent-border); color: var(--pi-accent); }
510
+ /* The selection survives a folder collapse: the nearest collapsed ancestor
511
+ highlights instead, without taking the fill reserved for the open document. */
512
+ .directory-tab.contains-active { color: var(--pi-accent); }
475
513
  .directory-tab.contains-active::after { content: ""; width: 5px; height: 5px; border-radius: 50%; background: var(--pi-accent); }
476
514
  code, pre { border: 1px solid var(--pi-border-muted); border-radius: 6px; background: var(--pi-bg); color: var(--pi-text-secondary); font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
477
515
  code { padding: 2px 5px; }