@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
@@ -17,13 +17,13 @@ const GIT_WORKSPACE_STATE_LIMIT = 8;
17
17
  const activityElementTag = "pi-web-git-panel-activity";
18
18
  const EMPTY_LIST_MODEL = { submodules: [], files: [] };
19
19
  const EMPTY_VIEW_STATE = { nodes: [], listModel: EMPTY_LIST_MODEL, expandablePaths: [] };
20
- export function createGitBrowserContributions(sourcePluginId, runtimePluginId, html, svg) {
20
+ export function createGitBrowserContributions(sourcePluginId, runtimePluginId, html, svg, stylesheetIntegration = () => undefined) {
21
21
  const panelId = `${runtimePluginId}:${GIT_PANEL_LOCAL_ID}`;
22
22
  const controller = new GitUiController(sourcePluginId, createGitDiffRoute(panelId));
23
23
  defineGitPanelActivityElement();
24
24
  return {
25
25
  actions: createGitActions(panelId, controller),
26
- workspacePanels: [createGitPanel(html, svg, controller)],
26
+ workspacePanels: [createGitPanel(html, svg, controller, stylesheetIntegration)],
27
27
  };
28
28
  }
29
29
  class GitUiController {
@@ -38,8 +38,8 @@ class GitUiController {
38
38
  this.sourcePluginId = sourcePluginId;
39
39
  this.route = route;
40
40
  }
41
- isOwnedWorkspace(workspace) {
42
- return workspace?.provider?.pluginId === this.sourcePluginId;
41
+ canInspectWorkspace(workspace) {
42
+ return workspace !== undefined && (workspace.provider === undefined || workspace.provider.pluginId === this.sourcePluginId);
43
43
  }
44
44
  state(context) {
45
45
  return this.stateFor(context);
@@ -83,7 +83,7 @@ class GitUiController {
83
83
  void this.refresh(context, false);
84
84
  }
85
85
  invalidate(context) {
86
- if (!this.isOwnedWorkspace(context.workspace))
86
+ if (!this.canInspectWorkspace(context.workspace))
87
87
  return Promise.resolve();
88
88
  const state = this.stateFor(context);
89
89
  state.stale = state.status !== undefined;
@@ -336,7 +336,7 @@ class GitUiController {
336
336
  }
337
337
  }
338
338
  function createGitActions(panelId, controller) {
339
- const hasGitWorkspace = (context) => controller.isOwnedWorkspace(context.state.selectedWorkspace);
339
+ const hasGitWorkspace = (context) => controller.canInspectWorkspace(context.state.selectedWorkspace);
340
340
  return [
341
341
  {
342
342
  id: "view.git",
@@ -358,7 +358,7 @@ function createGitActions(panelId, controller) {
358
358
  },
359
359
  ];
360
360
  }
361
- function createGitPanel(html, svg, controller) {
361
+ function createGitPanel(html, svg, controller, stylesheetIntegration) {
362
362
  return {
363
363
  id: GIT_PANEL_LOCAL_ID,
364
364
  title: "Git",
@@ -375,9 +375,9 @@ function createGitPanel(html, svg, controller) {
375
375
  order: 20,
376
376
  routeAliases: ["git", "core:workspace.git"],
377
377
  navigationAliases: ["core:workspace.git"],
378
- visible: (context) => controller.isOwnedWorkspace(context.workspace),
378
+ visible: (context) => controller.canInspectWorkspace(context.workspace),
379
379
  onInvalidate: (context) => controller.invalidate(context),
380
- render: (context) => renderGitPanel(html, controller, context),
380
+ render: (context) => renderGitPanel(html, controller, context, stylesheetIntegration()),
381
381
  };
382
382
  }
383
383
  function requestGitBackend(context, operation, input) {
@@ -386,26 +386,26 @@ function requestGitBackend(context, operation, input) {
386
386
  }
387
387
  return context.peer.request(operation, input);
388
388
  }
389
- function renderGitPanel(html, controller, context) {
389
+ function renderGitPanel(html, controller, context, stylesheetIntegration) {
390
390
  const state = controller.state(context);
391
391
  const viewState = controller.viewState(state);
392
392
  return html `
393
393
  <section class="git-panel">
394
- <style .textContent=${gitPanelStyles}></style>
395
- <pi-web-git-panel-activity .controller=${controller} .context=${context}></pi-web-git-panel-activity>
396
- <section class="git-toolbar">
394
+ <style data-git-styles .textContent=${gitPanelStyles}></style>
395
+ <pi-web-git-panel-activity .stylesheetIntegration=${stylesheetIntegration} .controller=${controller} .context=${context}></pi-web-git-panel-activity>
396
+ <section class="ui-toolbar git-toolbar">
397
397
  <strong>Git</strong>
398
398
  ${state.stale ? html `<span class="git-stale">${t("ui.main.3468df3f1fe9", "stale")}</span>` : null}
399
- <div class="git-toolbar-actions">
399
+ <div class="ui-toolbar-group git-toolbar-actions">
400
400
  ${viewState.expandablePaths.length === 0 ? null : renderExpandCollapseAll(html, controller, context, state, viewState.expandablePaths)}
401
401
  ${renderViewToggle(html, controller, context)}
402
- <button type="button" ?disabled=${state.statusLoading} @click=${() => { void controller.refresh(context); }}>${t("bundled.relays.refresh.aee887", "Refresh")}</button>
402
+ <button class="ui-button" type="button" ?disabled=${state.statusLoading} @click=${() => { void controller.refresh(context); }}>${t("bundled.relays.refresh.aee887", "Refresh")}</button>
403
403
  </div>
404
404
  </section>
405
405
  ${state.error === undefined ? null : html `<div class="git-error" role="alert">${state.error}</div>`}
406
406
  ${state.status?.truncated === true ? html `<div class="git-warning" role="status">${t("ui.main.1634590c49b8", "Git status output was too large; the file list was truncated.")}</div>` : null}
407
407
  <section class="git-split">
408
- <div class="git-file-list">${renderFileList(html, controller, context, state, viewState)}</div>
408
+ <div class="ui-file-list git-file-list">${renderFileList(html, controller, context, state, viewState)}</div>
409
409
  <div class="git-viewer">${renderDiffViewer(html, state)}</div>
410
410
  </section>
411
411
  </section>
@@ -413,7 +413,7 @@ function renderGitPanel(html, controller, context) {
413
413
  }
414
414
  function renderViewToggle(html, controller, context) {
415
415
  return html `
416
- <div class="git-view-toggle" role="group" aria-label="${t("ui.main.898449210b9d", "Changed files view")}">
416
+ <div class="ui-toolbar-group git-view-toggle" role="group" aria-label="${t("ui.main.898449210b9d", "Changed files view")}">
417
417
  ${renderViewToggleButton(html, controller, context, "list", t("ui.workspaceGitPanel.list", "List"))}
418
418
  ${renderViewToggleButton(html, controller, context, "tree", t("ui.main.7f32c63d2d35", "Tree"))}
419
419
  </div>
@@ -421,11 +421,11 @@ function renderViewToggle(html, controller, context) {
421
421
  }
422
422
  function renderViewToggleButton(html, controller, context, view, label) {
423
423
  const active = controller.currentView() === view;
424
- return html `<button type="button" class=${active ? "is-selected" : ""} aria-pressed=${String(active)} @click=${() => { controller.setView(context, view); }}>${label}</button>`;
424
+ return html `<button type="button" class="ui-button" aria-pressed=${String(active)} @click=${() => { controller.setView(context, view); }}>${label}</button>`;
425
425
  }
426
426
  function renderExpandCollapseAll(html, controller, context, state, expandablePaths) {
427
427
  const allExpanded = expandablePaths.every((path) => state.expandedDirectories.has(path));
428
- return html `<button type="button" @click=${() => { controller.toggleExpandAll(context, expandablePaths, allExpanded); }}>${allExpanded ? t("ui.workspaceGitPanel.collapseAll", "Collapse all") : t("ui.workspaceGitPanel.expandAll", "Expand all")}</button>`;
428
+ return html `<button class="ui-button" type="button" @click=${() => { controller.toggleExpandAll(context, expandablePaths, allExpanded); }}>${allExpanded ? t("ui.workspaceGitPanel.collapseAll", "Collapse all") : t("ui.workspaceGitPanel.expandAll", "Expand all")}</button>`;
429
429
  }
430
430
  function renderFileList(html, controller, context, state, viewState) {
431
431
  const status = state.status;
@@ -458,9 +458,9 @@ function renderSubmoduleGroup(html, controller, context, state, group) {
458
458
  return html ``;
459
459
  const expanded = state.expandedDirectories.has(group.path);
460
460
  return html `
461
- <button type="button" class="git-row" style="--depth:0" aria-expanded=${String(expanded)} @click=${() => { controller.toggleDirectory(context, group.path); }}>
462
- <span class="git-twisty">${expanded ? "▾" : "▸"}</span>
463
- <span>${group.name}${submoduleBadge(html)}</span>
461
+ <button type="button" class="ui-button ui-file-row" style="--depth:0" title=${group.path} aria-expanded=${String(expanded)} @click=${() => { controller.toggleDirectory(context, group.path); }}>
462
+ <span class="ui-file-leading" aria-hidden="true">${expanded ? "▾" : "▸"}</span>
463
+ <span class="ui-file-label">${group.name}${submoduleBadge(html)}</span>
464
464
  </button>
465
465
  ${expanded ? html `
466
466
  ${group.pointer === undefined ? null : renderSelectableRow(html, controller, context, state, group.path, group.pointer.name, group.pointer.file, 1)}
@@ -477,9 +477,9 @@ function renderTreeNode(html, controller, context, state, node, depth) {
477
477
  return html ``;
478
478
  const expanded = state.expandedDirectories.has(node.path);
479
479
  return html `
480
- <button type="button" class="git-row" style=${`--depth:${String(depth)}`} aria-expanded=${String(expanded)} @click=${() => { controller.toggleDirectory(context, node.path); }}>
481
- <span class="git-twisty">${expanded ? "▾" : "▸"}</span>
482
- <span>${node.name}${node.isSubmodule === true ? submoduleBadge(html) : null}</span>
480
+ <button type="button" class="ui-button ui-file-row" style=${`--depth:${String(depth)}`} title=${node.path} aria-expanded=${String(expanded)} @click=${() => { controller.toggleDirectory(context, node.path); }}>
481
+ <span class="ui-file-leading" aria-hidden="true">${expanded ? "▾" : "▸"}</span>
482
+ <span class="ui-file-label">${node.name}${node.isSubmodule === true ? submoduleBadge(html) : null}</span>
483
483
  </button>
484
484
  ${expanded ? node.children.map((child) => renderTreeNode(html, controller, context, state, child, depth + 1)) : null}
485
485
  `;
@@ -494,9 +494,10 @@ function renderSelectableRow(html, controller, context, state, path, label, file
494
494
  return html ``;
495
495
  const selected = state.selectedDiffPath === path;
496
496
  return html `
497
- <button type="button" class=${selected ? "git-row is-selected" : "git-row"} style=${`--depth:${String(depth)}`} @click=${() => { controller.selectDiff(context, path); }}>
498
- <span>${stateLabel(file.index, file.workingTree)}</span>
499
- <span>${label}</span>
497
+ <button type="button" class=${selected ? "ui-button ui-file-row selected" : "ui-button ui-file-row"} style=${`--depth:${String(depth)}`} title=${path} aria-current=${selected ? "page" : "false"} @click=${() => { controller.selectDiff(context, path); }}>
498
+ <span class="ui-file-leading" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"></path><path d="M14 2v6h6"></path></svg></span>
499
+ <span class="ui-file-label">${label}</span>
500
+ <span class="ui-file-meta">${stateLabel(file.index, file.workingTree)}</span>
500
501
  </button>
501
502
  `;
502
503
  }
@@ -591,6 +592,24 @@ function defineGitPanelActivityElement() {
591
592
  controllerValue;
592
593
  contextValue;
593
594
  pollTimer;
595
+ integration;
596
+ releaseStyles;
597
+ set stylesheetIntegration(value) {
598
+ if (this.integration === value)
599
+ return;
600
+ this.releaseStyles?.();
601
+ this.releaseStyles = undefined;
602
+ this.integration = value;
603
+ this.registerStyles();
604
+ }
605
+ registerStyles() {
606
+ if (!this.isConnected || this.releaseStyles !== undefined)
607
+ return;
608
+ const root = this.getRootNode();
609
+ // The activity element shares the host panel's root; it owns only this opt-in.
610
+ if (root instanceof ShadowRoot)
611
+ this.releaseStyles = this.integration?.registerRoot(root, { sharedUi: true });
612
+ }
594
613
  set controller(value) {
595
614
  if (this.controllerValue === value)
596
615
  return;
@@ -604,11 +623,14 @@ function defineGitPanelActivityElement() {
604
623
  this.restart();
605
624
  }
606
625
  connectedCallback() {
626
+ this.registerStyles();
607
627
  window.addEventListener("popstate", this.onPopState);
608
628
  document.addEventListener("visibilitychange", this.onVisibilityChange);
609
629
  this.restart();
610
630
  }
611
631
  disconnectedCallback() {
632
+ this.releaseStyles?.();
633
+ this.releaseStyles = undefined;
612
634
  window.removeEventListener("popstate", this.onPopState);
613
635
  document.removeEventListener("visibilitychange", this.onVisibilityChange);
614
636
  if (this.controllerValue !== undefined && this.contextValue !== undefined)
@@ -675,30 +697,22 @@ function errorMessage(error) {
675
697
  return error instanceof Error ? error.message : String(error);
676
698
  }
677
699
  const gitPanelStyles = `
678
- .git-panel { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; color: var(--pi-text); background: var(--pi-bg); font: 13px system-ui, sans-serif; }
700
+ .git-panel { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; color: var(--pi-text); background: var(--pi-bg); font: 13px var(--pi-font, system-ui, sans-serif); }
679
701
  .git-panel ${activityElementTag} { display: none; }
680
- .git-panel button { display: inline-flex; align-items: center; gap: 5px; border: 1px solid var(--pi-border); border-radius: 7px; background: var(--pi-surface); color: var(--pi-text); padding: 5px 7px; cursor: pointer; }
681
- .git-panel button:disabled { cursor: wait; opacity: .65; }
682
702
  .git-panel small, .git-panel .git-muted { color: var(--pi-muted); }
683
703
  .git-panel p { margin: 10px; }
684
- .git-panel .git-toolbar { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 8px; border-bottom: 1px solid var(--pi-border-muted); }
685
- .git-panel .git-toolbar-actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }
686
- .git-panel .git-view-toggle { display: inline-flex; }
687
- .git-panel .git-view-toggle button { border-radius: 0; }
688
- .git-panel .git-view-toggle button:first-child { border-top-left-radius: 7px; border-bottom-left-radius: 7px; }
689
- .git-panel .git-view-toggle button:last-child { margin-left: -1px; border-top-right-radius: 7px; border-bottom-right-radius: 7px; }
690
- .git-panel .git-view-toggle button.is-selected { position: relative; z-index: 1; border-color: var(--pi-accent); background: var(--pi-selection-bg); }
691
- .git-panel .git-stale { border: 1px solid var(--pi-warning-border); border-radius: 999px; color: var(--pi-warning); padding: 1px 6px; font-size: 12px; }
704
+ .git-panel .git-toolbar { flex: 0 0 auto; border-bottom: 1px solid var(--pi-border-muted); font-size: 12px; color: var(--pi-muted); }
705
+ .git-panel .git-toolbar-actions { margin-left: auto; }
706
+ .git-panel .git-toolbar > strong { display: flex; align-items: center; padding-inline: 10px; font-weight: 500; }
707
+ .git-panel .git-toolbar .ui-button { white-space: nowrap; }
708
+ .git-panel .git-toolbar .ui-button[aria-pressed="true"] { background: var(--pi-surface-hover); color: var(--pi-text); }
709
+ .git-panel .git-stale { border: 1px solid var(--pi-warning-border); border-radius: 999px; color: var(--pi-warning); padding: 1px 6px; font-size: 12px; align-self: center; }
692
710
  .git-panel .git-error { flex: 0 0 auto; margin: 8px; border: 1px solid var(--pi-danger); border-radius: 7px; color: var(--pi-danger); padding: 8px; }
693
711
  .git-panel .git-warning { flex: 0 0 auto; margin: 8px; border: 1px solid var(--pi-warning-border); border-radius: 7px; color: var(--pi-warning); padding: 8px; }
694
712
  .git-panel .git-load-more { margin: 8px auto; }
695
713
  .git-panel .git-split { flex: 1 1 auto; min-height: 0; display: grid; grid-template-rows: minmax(160px, 34%) minmax(0, 1fr); }
696
- .git-panel .git-file-list { min-height: 0; overflow: auto; border-bottom: 1px solid var(--pi-border); padding: 6px; }
697
- .git-panel .git-row { display: grid; grid-template-columns: 18px minmax(0, 1fr); gap: 4px; width: 100%; border: 0; border-radius: 5px; background: transparent; text-align: left; padding: 4px 6px 4px calc(6px + var(--depth, 0) * 14px); }
698
- .git-panel .git-row:hover, .git-panel .git-row.is-selected { background: var(--pi-selection-bg); }
699
- .git-panel .git-row span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
700
- .git-panel .git-twisty { color: var(--pi-dim, var(--pi-muted)); }
701
- .git-panel .git-summary { margin: 4px 6px 8px; color: var(--pi-muted); }
714
+ .git-panel .git-file-list { border-bottom: 1px solid var(--pi-border); }
715
+ .git-panel .git-summary { margin: 10px 12px 8px; color: var(--pi-muted); }
702
716
  .git-panel .submodule-badge { display: inline-block; margin-left: 6px; border: 1px solid var(--pi-border); border-radius: 999px; color: var(--pi-muted); padding: 0 5px; font-size: 11px; font-weight: 400; vertical-align: baseline; }
703
717
  .git-panel .git-viewer { min-height: 0; overflow: auto; display: flex; flex-direction: column; }
704
718
  .git-panel .git-diffs { flex: 1 1 auto; min-height: 0; overflow: auto; display: grid; grid-template-rows: minmax(120px, 1fr) minmax(120px, 1fr); }
@@ -708,7 +722,7 @@ const gitPanelStyles = `
708
722
  .git-panel .git-viewer-header { position: sticky; top: 0; display: flex; justify-content: space-between; gap: 8px; padding: 8px; border-bottom: 1px solid var(--pi-border-muted); background: var(--pi-bg); }
709
723
  .git-panel .git-viewer-header strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
710
724
  .git-panel .git-diff-scroller { flex: 1 1 auto; min-height: 0; overflow: auto; background: var(--pi-bg); }
711
- .git-panel .git-diff-grid { display: grid; grid-template-columns: max-content max-content 2ch max-content; width: max-content; min-width: 100%; padding: 6px 0; font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; }
725
+ .git-panel .git-diff-grid { display: grid; grid-template-columns: max-content max-content 2ch max-content; width: max-content; min-width: 100%; padding: 6px 0; font: 12px var(--pi-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); line-height: 1.45; }
712
726
  .git-panel .git-diff-line { display: contents; }
713
727
  .git-panel .git-diff-cell { min-height: 1.45em; white-space: pre; }
714
728
  .git-panel .git-line-number { min-width: 4ch; padding: 0 8px; border-right: 1px solid var(--pi-border-muted); color: var(--pi-dim); text-align: right; user-select: none; }
@@ -1,14 +1,34 @@
1
1
  // Generated from pi-web-plugins/git/browser/pi-web-plugin.ts. Do not edit directly.
2
2
  import { setTranslator } from "./i18n.js";
3
3
  import { createGitBrowserContributions } from "./git-panel.js";
4
+ const stylesheetIntegrationCapability = {
5
+ pluginId: "pi-web", id: "stylesheet-integration", version: 1,
6
+ parse: (value) => {
7
+ if (!isStylesheetIntegration(value)) {
8
+ throw new Error("Invalid PI WEB stylesheet integration capability v1");
9
+ }
10
+ return value;
11
+ },
12
+ };
13
+ function isStylesheetIntegration(value) {
14
+ return typeof value === "object" && value !== null
15
+ && "registerRoot" in value && typeof value.registerRoot === "function"
16
+ && "subscribe" in value && typeof value.subscribe === "function"
17
+ && "createController" in value && typeof value.createController === "function";
18
+ }
4
19
  const plugin = {
5
20
  apiVersion: 4,
6
21
  name: "Git",
22
+ requires: [stylesheetIntegrationCapability],
7
23
  activate: ({ pluginId, runtimePluginId, html, svg, translate }) => {
8
24
  setTranslator(translate);
9
- return ({
10
- contributions: createGitBrowserContributions(pluginId, runtimePluginId, html, svg),
11
- });
25
+ let stylesheetIntegration;
26
+ return {
27
+ start: ({ capabilities }) => {
28
+ stylesheetIntegration = capabilities.resolve(stylesheetIntegrationCapability);
29
+ },
30
+ contributions: createGitBrowserContributions(pluginId, runtimePluginId, html, svg, () => stylesheetIntegration),
31
+ };
12
32
  },
13
33
  };
14
34
  export default plugin;
@@ -11,11 +11,19 @@ const statusCaches = new WeakMap();
11
11
  const GIT_LOCAL_ENV_VARS = Object.freeze([
12
12
  "GIT_ALTERNATE_OBJECT_DIRECTORIES",
13
13
  "GIT_COMMON_DIR",
14
+ "GIT_CONFIG",
15
+ "GIT_CONFIG_COUNT",
16
+ "GIT_CONFIG_PARAMETERS",
14
17
  "GIT_DIR",
18
+ "GIT_GRAFT_FILE",
19
+ "GIT_IMPLICIT_WORK_TREE",
15
20
  "GIT_INDEX_FILE",
21
+ "GIT_NO_REPLACE_OBJECTS",
16
22
  "GIT_OBJECT_DIRECTORY",
17
23
  "GIT_PREFIX",
18
24
  "GIT_QUARANTINE_PATH",
25
+ "GIT_REPLACE_REF_BASE",
26
+ "GIT_SHALLOW_FILE",
19
27
  "GIT_WORK_TREE",
20
28
  ]);
21
29
  /** Dispatch the Git status/diff schema through the package peer request seam. */
@@ -44,7 +52,7 @@ async function gitStatusWithRunner(runGit, cwd) {
44
52
  if (result.code !== 0)
45
53
  return { isGitRepo: false, hash: hash(result.stdout + result.stderr), files: [], submodules: [], ...(result.truncated ? { truncated: true } : {}) };
46
54
  const parsed = parseStatus(result.truncated ? result.stdout.slice(0, result.stdout.lastIndexOf("\0") + 1) : result.stdout, { deferSubmodules: true });
47
- return expandSubmodules(runGit, cwd, parsed, result.stdout, result.truncated);
55
+ return expandSubmodules(runGit, await gitWorktreeRoot(runGit, cwd), parsed, result.stdout, result.truncated);
48
56
  }
49
57
  /**
50
58
  * Merge each dirty submodule's own changes into the flat file list. A moved
@@ -136,6 +144,8 @@ async function gitDiffWithRunner(runGit, cwd, options) {
136
144
  let path;
137
145
  if (options.path !== undefined && options.path !== "")
138
146
  path = normalizeRelativePath(options.path);
147
+ // Diff/status paths belong to the repository, not a folder project's session cwd.
148
+ cwd = await gitWorktreeRoot(runGit, cwd);
139
149
  if (path !== undefined) {
140
150
  const owner = await submoduleForPath(runGit, cwd, path);
141
151
  if (owner !== undefined)
@@ -181,6 +191,15 @@ async function submoduleDiff(runGit, owner, path, staged) {
181
191
  }
182
192
  return { path, staged, hash: hash(result.stdout), diff: result.stdout, truncated: result.truncated };
183
193
  }
194
+ async function gitWorktreeRoot(runGit, cwd) {
195
+ const result = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
196
+ if (result.code !== 0)
197
+ throw new Error(result.stderr.trim() || t("git.cwdRootUnavailable", "Cannot resolve Git worktree root"));
198
+ const root = result.stdout.replace(/\r?\n$/u, "");
199
+ if (!isAbsolute(root))
200
+ throw new Error(t("git.cwdRootInvalid", "Git returned an invalid worktree root"));
201
+ return root;
202
+ }
184
203
  async function isUntracked(runGit, cwd, path) {
185
204
  const result = await runGit(cwd, ["ls-files", "--others", "--exclude-standard", "-z", "--", path]);
186
205
  return result.code === 0 && result.stdout.split("\0").includes(path);
@@ -2,14 +2,23 @@
2
2
  import { t, setTranslator } from "./i18n.js";
3
3
  import { basename, resolve } from "node:path";
4
4
  import { requestGitBackend } from "./git-backend.js";
5
+ import { createWorkspaceDiscoveryHooks } from "./workspaceDiscoveryHooks.js";
5
6
  const GIT_LOCAL_ENV_VARS = Object.freeze([
6
7
  "GIT_ALTERNATE_OBJECT_DIRECTORIES",
7
8
  "GIT_COMMON_DIR",
9
+ "GIT_CONFIG",
10
+ "GIT_CONFIG_COUNT",
11
+ "GIT_CONFIG_PARAMETERS",
8
12
  "GIT_DIR",
13
+ "GIT_GRAFT_FILE",
14
+ "GIT_IMPLICIT_WORK_TREE",
9
15
  "GIT_INDEX_FILE",
16
+ "GIT_NO_REPLACE_OBJECTS",
10
17
  "GIT_OBJECT_DIRECTORY",
11
18
  "GIT_PREFIX",
12
19
  "GIT_QUARANTINE_PATH",
20
+ "GIT_REPLACE_REF_BASE",
21
+ "GIT_SHALLOW_FILE",
13
22
  "GIT_WORK_TREE",
14
23
  ]);
15
24
  const plugin = {
@@ -18,22 +27,29 @@ const plugin = {
18
27
  activate(context) {
19
28
  setTranslator(context.translate);
20
29
  return {
21
- workspaceProvider: createGitWorkspaceProvider(context),
30
+ workspaceProvider: createGitWorkspaceProvider(context, process.env["PI_WEB_INSTANCE_NAMESPACE"] ?? ""),
22
31
  peer: { request: (request) => requestGitBackend(context, request) },
23
32
  };
24
33
  },
25
34
  };
26
35
  export default plugin;
27
- export function createGitWorkspaceProvider(context) {
36
+ export function createGitWorkspaceProvider(context, instanceNamespace = "") {
28
37
  return Object.freeze({
29
38
  fallback: true,
39
+ integration: createWorkspaceDiscoveryHooks(context, (cwd, args, signal) => runGit(context, cwd, args, signal), process.execPath, instanceNamespace),
30
40
  async probe(project, signal) {
31
41
  const result = await runGit(context, project.path, ["rev-parse", "--is-inside-work-tree"], signal);
32
42
  if (result.signal !== null)
33
43
  throw new Error(t("ui.main.8919cbc9c0bd", "Git repository probe ended with signal {arg0}", { arg0: result.signal }));
34
44
  if (result.exitCode !== 0)
35
45
  return "pass";
36
- return result.stdout.trim() === "true" ? "claim" : "pass";
46
+ if (result.stdout.trim() !== "true")
47
+ return "pass";
48
+ const root = await requireGit(runGit(context, project.path, ["rev-parse", "--show-toplevel"], signal), t("ui.main.49c35c822771", "resolve the Git worktree root"));
49
+ if (root.stdout.trim() === "")
50
+ throw new Error(t("ui.main.f1639c9e2fd7", "Git returned an empty worktree root"));
51
+ // A folder project keeps its cwd; only checkout roots own worktree discovery.
52
+ return resolve(root.stdout.trim()) === resolve(project.path) ? "claim" : "pass";
37
53
  },
38
54
  async list(project, signal) {
39
55
  const rootResult = await requireGit(runGit(context, project.path, ["rev-parse", "--show-toplevel"], signal), t("ui.main.49c35c822771", "resolve the Git worktree root"));
@@ -163,6 +179,7 @@ async function runGit(context, cwd, args, signal) {
163
179
  file: "git",
164
180
  args: ["-C", cwd, ...args],
165
181
  unsetEnv: GIT_LOCAL_ENV_VARS,
182
+ timeoutMs: 10_000,
166
183
  signal,
167
184
  });
168
185
  if (result.stdoutTruncated || result.stderrTruncated) {
@@ -0,0 +1,14 @@
1
+ // Packaged with PI WEB: best-effort discovery hint, never Git authority.
2
+ const { spawnSync } = require("node:child_process");
3
+ const [notification, ...args] = process.argv.slice(2);
4
+
5
+ // Initial worktree checkout has a null previous OID and branch flag 1.
6
+ // --no-checkout skips this hook; ordinary catalog access still discovers it.
7
+ if (args.length === 3 && /^(?:0{40}|0{64})$/.test(args[0]) && args[2] === "1") {
8
+ try {
9
+ const command = JSON.parse(notification);
10
+ if (Array.isArray(command) && command.length > 0 && command.every((value) => typeof value === "string")) {
11
+ spawnSync(command[0], command.slice(1), { stdio: "ignore", timeout: 2000, killSignal: "SIGKILL" });
12
+ }
13
+ } catch { /* Invalid arguments, missing commands, and daemon failures must not affect Git. */ }
14
+ }
@@ -0,0 +1,186 @@
1
+ // Generated from pi-web-plugins/git/workspaceDiscoveryHooks.ts. Do not edit directly.
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join, resolve } from "node:path";
5
+ const EVENT = "post-checkout";
6
+ /** Git owns the hook policy; the host owns settings snapshots and notification attribution/transport. */
7
+ export function createWorkspaceDiscoveryHooks(context, runGit, nodeExecutable = process.execPath, instanceNamespace = "") {
8
+ if (instanceNamespace !== "" && !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/u.test(instanceNamespace)) {
9
+ throw new Error("PI_WEB_INSTANCE_NAMESPACE must be 1–32 letters, digits, underscores or hyphens, starting with a letter or digit");
10
+ }
11
+ if (!isAbsolute(nodeExecutable))
12
+ throw new Error("Git hook runtime must be an absolute executable path");
13
+ // Serialize this activation's config edits, including registrations sharing a
14
+ // common Git config. Git itself locks writes from other processes.
15
+ let pending = Promise.resolve();
16
+ return Object.freeze({
17
+ reconcile(project, signal) {
18
+ const operation = pending.then(() => reconcile(project, signal));
19
+ pending = operation.catch(() => { });
20
+ return operation;
21
+ },
22
+ async acceptNotification(project, signal) {
23
+ return await enabled(project, signal) && (context.workspaceNotifications?.command(project.id).length ?? 0) > 0;
24
+ },
25
+ });
26
+ async function enabled(project, signal) {
27
+ signal.throwIfAborted();
28
+ const settings = context.resolveProjectSettings === undefined
29
+ ? context.settings
30
+ : await context.resolveProjectSettings(project, signal);
31
+ const value = settings["workspaceDiscoveryHooks"];
32
+ if (value === undefined)
33
+ return true;
34
+ if (typeof value !== "boolean")
35
+ throw new Error("Git setting workspaceDiscoveryHooks must be a boolean");
36
+ return value;
37
+ }
38
+ async function reconcile(project, signal) {
39
+ const isEnabled = await enabled(project, signal);
40
+ // Separate projects/instances may share Git config. This identity belongs
41
+ // to the packaged-helper implementation; older generated helpers are untouched.
42
+ const identity = createHash("sha256")
43
+ .update(JSON.stringify(["packaged-helper", context.pluginId, resolve(context.dataDirectory), project.id]))
44
+ .digest("hex");
45
+ // Keep the unnamespaced production identity byte-for-byte compatible.
46
+ // The name also scopes ownership records; never adopt another instance's hook.
47
+ const prefix = instanceNamespace === "" ? "" : `${instanceNamespace}-`;
48
+ const name = `pi-web-workspace-discovery-${prefix}${identity}`;
49
+ const helper = join(context.packageRoot, "workspace-discovery-notify.cjs");
50
+ const registration = join(context.dataDirectory, "workspace-discovery", `${name}.registration.json`);
51
+ const notifier = context.workspaceNotifications?.command(project.id);
52
+ const command = registrationCommand(helper, nodeExecutable, notifier ?? []);
53
+ const ownedCommands = new Set([command, ...await readRegistrationCommands(registration, signal)]);
54
+ const install = isEnabled && notifier !== undefined && notifier.length > 0;
55
+ if (install && !await supportsNamedHooks(project, signal))
56
+ return;
57
+ const commandKey = `hook.${name}.command`;
58
+ const eventKey = `hook.${name}.event`;
59
+ const commands = await readConfig(project, commandKey, signal);
60
+ const events = await readConfig(project, eventKey, signal);
61
+ if (commands.some((entry) => entry.scope !== "local" || !ownedCommands.has(entry.value))
62
+ || events.some((entry) => entry.scope !== "local" || (entry.value !== EVENT && commands.length === 0))) {
63
+ // We only install local keys. A same-name global/worktree registration is
64
+ // external even if its values happen to match ours; never override it or
65
+ // remove the local command on which its inherited event might depend.
66
+ throw new Error(`Git workspace discovery hook conflict: ${name} has unexpected configuration`);
67
+ }
68
+ if (!install) {
69
+ // Disconnect the event first: an event without a command makes Git fail.
70
+ // Installation uses the reverse order for the same reason.
71
+ await unsetLocal(project, eventKey, events, signal);
72
+ await unsetLocal(project, commandKey, commands, signal);
73
+ signal.throwIfAborted();
74
+ await rm(registration, { force: true });
75
+ return;
76
+ }
77
+ // Routing/runtime arguments live in Git config, not generated executable
78
+ // files. Preserve exact ownership before changing config, including either
79
+ // side of a failed update, then compact after installation succeeds.
80
+ await writeRegistration([...new Set([...commands.map((entry) => entry.value), command])]);
81
+ await setLocal(project, commandKey, command, commands, signal);
82
+ await setLocal(project, eventKey, EVENT, events, signal);
83
+ await writeRegistration([command]);
84
+ async function writeRegistration(owned) {
85
+ await writeOwnedFileIfChanged(registration, `${JSON.stringify({ version: 1, commands: owned })}\n`, signal);
86
+ }
87
+ }
88
+ async function supportsNamedHooks(project, signal) {
89
+ // Detect Git's advertised named-config facility, not its version. `hook
90
+ // list` is unsuitable here: a missing command in an existing registration
91
+ // makes it fail, preventing precisely the repair we need to perform.
92
+ const result = await runGit(project.path, ["help", "--config"], signal);
93
+ if (result.signal !== null)
94
+ throw new Error(`Git named-hook detection ended from signal ${result.signal}`);
95
+ const keys = result.stdout.split(/\r?\n/u);
96
+ const supported = result.exitCode === 0
97
+ && keys.includes("hook.<friendly-name>.command") && keys.includes("hook.<friendly-name>.event");
98
+ if (!supported)
99
+ context.logger.debug("Git named configuration hooks unavailable; workspace discovery remains on catalog access", { projectId: project.id });
100
+ return supported;
101
+ }
102
+ async function readConfig(project, key, signal) {
103
+ const result = await runGit(project.path, ["config", "--null", "--show-scope", "--get-all", key], signal);
104
+ if (result.signal === null && result.exitCode === 1)
105
+ return [];
106
+ requireSuccess(result, `read ${key}`);
107
+ const fields = result.stdout.split("\0");
108
+ if (fields.pop() !== "" || fields.length % 2 !== 0)
109
+ throw new Error(`Malformed Git config output for ${key}`);
110
+ const entries = [];
111
+ for (let index = 0; index < fields.length; index += 2) {
112
+ entries.push({ scope: fields[index] ?? "", value: fields[index + 1] ?? "" });
113
+ }
114
+ return entries;
115
+ }
116
+ async function setLocal(project, key, value, entries, signal) {
117
+ const local = entries.filter((entry) => entry.scope === "local");
118
+ if (local.length === 1 && local[0]?.value === value)
119
+ return;
120
+ requireSuccess(await runGit(project.path, ["config", "--local", "--replace-all", key, value], signal), `set ${key}`);
121
+ }
122
+ async function unsetLocal(project, key, entries, signal) {
123
+ if (!entries.some((entry) => entry.scope === "local"))
124
+ return;
125
+ requireSuccess(await runGit(project.path, ["config", "--local", "--unset-all", key], signal), `remove ${key}`);
126
+ }
127
+ }
128
+ function requireSuccess(result, action) {
129
+ if (result.signal === null && result.exitCode === 0)
130
+ return;
131
+ const outcome = result.signal === null ? `exit ${String(result.exitCode)}` : `signal ${result.signal}`;
132
+ throw new Error(`Unable to ${action} (${outcome}): ${result.stderr.trim()}`);
133
+ }
134
+ function shellQuote(value) {
135
+ return `'${value.replaceAll("'", "'\\''")}'`;
136
+ }
137
+ function registrationCommand(helper, nodeExecutable, notificationCommand) {
138
+ // Git itself supplies the shell and appends quoted hook argv to the function
139
+ // call. No extra `sh` executable is required on the user's PATH. The final
140
+ // return preserves best-effort behavior even when our helper/runtime is gone.
141
+ const shellHelper = process.platform === "win32" ? helper.replaceAll("\\", "/") : helper;
142
+ const shellNode = process.platform === "win32" ? nodeExecutable.replaceAll("\\", "/") : nodeExecutable;
143
+ return `pi_web_workspace_notify () { if test -r ${shellQuote(shellHelper)}; then ${shellQuote(shellNode)} ${shellQuote(shellHelper)} ${shellQuote(JSON.stringify(notificationCommand))} "$@" >/dev/null 2>&1; fi; return 0; }; pi_web_workspace_notify`;
144
+ }
145
+ async function readRegistrationCommands(path, signal) {
146
+ let source;
147
+ try {
148
+ source = await readFile(path, { encoding: "utf8", signal });
149
+ }
150
+ catch (error) {
151
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
152
+ return [];
153
+ throw error;
154
+ }
155
+ const value = JSON.parse(source);
156
+ if (typeof value !== "object" || value === null || Array.isArray(value)
157
+ || Reflect.get(value, "version") !== 1)
158
+ throw new Error(`Invalid PI WEB Git hook registration record: ${path}`);
159
+ const commands = Reflect.get(value, "commands");
160
+ if (!Array.isArray(commands) || !commands.every((command) => typeof command === "string" && command.length > 0)) {
161
+ throw new Error(`Invalid PI WEB Git hook command record: ${path}`);
162
+ }
163
+ return commands;
164
+ }
165
+ async function writeOwnedFileIfChanged(path, source, signal) {
166
+ signal.throwIfAborted();
167
+ try {
168
+ if (await readFile(path, { encoding: "utf8", signal }) === source)
169
+ return;
170
+ }
171
+ catch (error) {
172
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT")
173
+ throw error;
174
+ }
175
+ await mkdir(dirname(path), { recursive: true });
176
+ // Atomic per-project ownership records never contain executable code.
177
+ const temporary = `${path}.${randomUUID()}.tmp`;
178
+ try {
179
+ await writeFile(temporary, source, { flag: "wx", mode: 0o600, signal });
180
+ signal.throwIfAborted();
181
+ await rename(temporary, path);
182
+ }
183
+ finally {
184
+ await rm(temporary, { force: true });
185
+ }
186
+ }
@@ -210,7 +210,7 @@ function renderWorkspaceSection(html, workspace) {
210
210
  </section>
211
211
  `;
212
212
  }
213
- /** Panel body: render the Info tab for the current workspace panel context. */
213
+ /** Panel body: machine status is useful even without a selected workspace. */
214
214
  export function renderInfoPanel(html, context) {
215
215
  return html `
216
216
  <style>
@@ -226,9 +226,9 @@ export function renderInfoPanel(html, context) {
226
226
  </style>
227
227
  <section class="toolbar"><strong>${t("bundled.info.info.e70286", "Info")}</strong></section>
228
228
  <section class="viewer info-status">
229
- ${renderStatusSection(html, context.state?.piWebStatus)}
229
+ ${renderStatusSection(html, context.state.piWebStatus)}
230
230
  ${renderMachineSection(html, context.machine)}
231
- ${renderWorkspaceSection(html, context.workspace)}
231
+ ${context.workspace === undefined ? null : renderWorkspaceSection(html, context.workspace)}
232
232
  </section>
233
233
  `;
234
234
  }
@@ -24,7 +24,7 @@ const plugin = {
24
24
  items: (context) => [{ type: "text", text: context.workspace.provider?.pluginId ?? (context.workspace.isGitRepo === true ? "Git" : t("bundled.info.folder.7c7802", "Folder")), title: context.workspace.path }],
25
25
  },
26
26
  ],
27
- workspacePanels: [
27
+ applicationPanels: [
28
28
  {
29
29
  id: "workspace.info",
30
30
  title: t("bundled.info.info.e70286", "Info"),