@jmfederico/pi-web 1.202608.0 → 1.202608.2

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 (258) hide show
  1. package/README.md +4 -4
  2. package/dist/cli.js +287 -113
  3. package/dist/cli.js.map +1 -1
  4. package/dist/client/assets/CodeViewer-BMWwxG7q.js +4 -0
  5. package/dist/client/assets/{TerminalPanel-fYrKc3jh.js → TerminalPanel-CacQDIYn.js} +6 -6
  6. package/dist/client/assets/index-DUW2xnoV.js +4279 -0
  7. package/dist/client/assets/vendor-editor-core-CXO8gGab.js +12 -0
  8. package/dist/client/assets/vendor-editor-languages-CpW4sJsX.js +46 -0
  9. package/dist/client/index.html +3 -3
  10. package/dist/config.js +110 -73
  11. package/dist/config.js.map +1 -1
  12. package/dist/docker/piWebDockerCommandPlan.js +4 -3
  13. package/dist/docker/piWebDockerCommandPlan.js.map +1 -1
  14. package/dist/environment.js +4 -0
  15. package/dist/environment.js.map +1 -0
  16. package/dist/nativeServices/installedServiceDefinitions.js +602 -0
  17. package/dist/nativeServices/installedServiceDefinitions.js.map +1 -0
  18. package/dist/nativeServices/serviceAction.js +174 -0
  19. package/dist/nativeServices/serviceAction.js.map +1 -0
  20. package/dist/nativeServices/serviceDoctor.js +374 -71
  21. package/dist/nativeServices/serviceDoctor.js.map +1 -1
  22. package/dist/nativeServices/servicePlan.js +1 -1
  23. package/dist/nativeServices/servicePlan.js.map +1 -1
  24. package/dist/{pi-web-plugins → pi-packages}/relays/markdownDocument.js +1 -1
  25. package/dist/pi-packages/relays/package.json +11 -0
  26. package/dist/{pi-web-plugins → pi-packages}/relays/pi-web-plugin.js +4 -4
  27. package/dist/pi-packages/relays/prompts/relay-worktree.md +157 -0
  28. package/dist/pi-packages/relays/prompts/relay.md +152 -0
  29. package/dist/{pi-web-plugins → pi-packages}/relays/relayDiscovery.js +1 -1
  30. package/dist/{pi-web-plugins → pi-packages}/relays/relaysPanelElement.js +1 -1
  31. package/dist/pi-packages/relays/skills/relay/SKILL.md +123 -0
  32. package/dist/pi-web-plugins/git/browser/git-contract.js +108 -0
  33. package/dist/pi-web-plugins/git/browser/git-panel.js +609 -0
  34. package/dist/pi-web-plugins/git/browser/gitFileList.js +43 -0
  35. package/dist/pi-web-plugins/git/browser/gitFileShared.js +16 -0
  36. package/dist/pi-web-plugins/git/browser/gitFileTree.js +81 -0
  37. package/dist/pi-web-plugins/git/browser/gitFileViewPreference.js +35 -0
  38. package/dist/pi-web-plugins/git/browser/gitRoute.js +42 -0
  39. package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +10 -0
  40. package/dist/pi-web-plugins/git/browser/unifiedDiff.js +301 -0
  41. package/dist/{server/git/gitService.js → pi-web-plugins/git/git-backend.js} +209 -60
  42. package/dist/pi-web-plugins/git/package.json +16 -0
  43. package/dist/pi-web-plugins/git/server-plugin.js +176 -0
  44. package/dist/pi-web-plugins/info/infoInternals.js +16 -2
  45. package/dist/pi-web-plugins/info/package.json +1 -1
  46. package/dist/pi-web-plugins/info/pi-web-plugin.js +2 -2
  47. package/dist/pi-web-plugins/updates/package.json +1 -1
  48. package/dist/pi-web-plugins/updates/pi-web-plugin.js +1 -1
  49. package/dist/pi-web-plugins/workspace-tasks/package.json +1 -1
  50. package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +3 -3
  51. package/dist/piWebVersionReport.js +63 -10
  52. package/dist/piWebVersionReport.js.map +1 -1
  53. package/dist/plugin-api.d.ts +33 -16
  54. package/dist/pluginRecoveryCli.js +172 -0
  55. package/dist/pluginRecoveryCli.js.map +1 -0
  56. package/dist/server/activity/workspaceActivityService.js +27 -19
  57. package/dist/server/activity/workspaceActivityService.js.map +1 -1
  58. package/dist/server/app.js +41 -15
  59. package/dist/server/app.js.map +1 -1
  60. package/dist/server/configRoutes.js +3 -21
  61. package/dist/server/configRoutes.js.map +1 -1
  62. package/dist/server/knownAutoInstallPiPackages.js +30 -0
  63. package/dist/server/knownAutoInstallPiPackages.js.map +1 -0
  64. package/dist/server/machines/machineClient.js +23 -4
  65. package/dist/server/machines/machineClient.js.map +1 -1
  66. package/dist/server/machines/machinePluginProxyRoutes.js +74 -17
  67. package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
  68. package/dist/server/machines/machineProxyRoutes.js +165 -8
  69. package/dist/server/machines/machineProxyRoutes.js.map +1 -1
  70. package/dist/server/machines/machineService.js +23 -0
  71. package/dist/server/machines/machineService.js.map +1 -1
  72. package/dist/server/piPackageIdentity.js +25 -0
  73. package/dist/server/piPackageIdentity.js.map +1 -0
  74. package/dist/server/piPackageService.js +69 -8
  75. package/dist/server/piPackageService.js.map +1 -1
  76. package/dist/server/piWebPluginCatalog.js +584 -0
  77. package/dist/server/piWebPluginCatalog.js.map +1 -0
  78. package/dist/server/piWebPluginLifecycle.js +162 -0
  79. package/dist/server/piWebPluginLifecycle.js.map +1 -0
  80. package/dist/server/piWebPluginService.js +159 -261
  81. package/dist/server/piWebPluginService.js.map +1 -1
  82. package/dist/server/piWebStatus.js +41 -9
  83. package/dist/server/piWebStatus.js.map +1 -1
  84. package/dist/server/plugins/pluginBackendProxyRoutes.js +69 -0
  85. package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -0
  86. package/dist/server/plugins/serverPluginExec.js +184 -0
  87. package/dist/server/plugins/serverPluginExec.js.map +1 -0
  88. package/dist/server/plugins/serverPluginRuntime.js +480 -0
  89. package/dist/server/plugins/serverPluginRuntime.js.map +1 -0
  90. package/dist/server/projectTrustRoutes.js +80 -0
  91. package/dist/server/projectTrustRoutes.js.map +1 -0
  92. package/dist/server/projects/projectService.js +3 -1
  93. package/dist/server/projects/projectService.js.map +1 -1
  94. package/dist/server/realtime/sessionEventHub.js +26 -11
  95. package/dist/server/realtime/sessionEventHub.js.map +1 -1
  96. package/dist/server/requestCancellation.js +32 -0
  97. package/dist/server/requestCancellation.js.map +1 -0
  98. package/dist/server/sessiond/agentProcessEnvironment.js +29 -30
  99. package/dist/server/sessiond/agentProcessEnvironment.js.map +1 -1
  100. package/dist/server/sessiond/autoInstallPiPackages.js +63 -0
  101. package/dist/server/sessiond/autoInstallPiPackages.js.map +1 -0
  102. package/dist/server/sessiond/pluginBackendRoutes.js +64 -0
  103. package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -0
  104. package/dist/server/sessiond/sessionDaemonShutdown.js +24 -0
  105. package/dist/server/sessiond/sessionDaemonShutdown.js.map +1 -0
  106. package/dist/server/sessiond/sessionProxyRoutes.js +2 -2
  107. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  108. package/dist/server/sessiond/sessionServiceDependencies.js +2 -1
  109. package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
  110. package/dist/server/sessiond/sessiondStateOwnership.js +235 -0
  111. package/dist/server/sessiond/sessiondStateOwnership.js.map +1 -0
  112. package/dist/server/sessiond/workspaceCatalogRoutes.js +31 -0
  113. package/dist/server/sessiond/workspaceCatalogRoutes.js.map +1 -0
  114. package/dist/server/sessiond/workspaceRemovalRoutes.js +41 -0
  115. package/dist/server/sessiond/workspaceRemovalRoutes.js.map +1 -0
  116. package/dist/server/sessiond.js +284 -98
  117. package/dist/server/sessiond.js.map +1 -1
  118. package/dist/server/sessions/authService.js +15 -54
  119. package/dist/server/sessions/authService.js.map +1 -1
  120. package/dist/server/sessions/dockerEnvironmentFacts.js +254 -0
  121. package/dist/server/sessions/dockerEnvironmentFacts.js.map +1 -0
  122. package/dist/server/sessions/modelCatalogRefresher.js +4 -3
  123. package/dist/server/sessions/modelCatalogRefresher.js.map +1 -1
  124. package/dist/server/sessions/piSessionManagerGateway.js +18 -67
  125. package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
  126. package/dist/server/sessions/piSessionService.js +495 -142
  127. package/dist/server/sessions/piSessionService.js.map +1 -1
  128. package/dist/server/sessions/sessionCommandService.js +24 -1
  129. package/dist/server/sessions/sessionCommandService.js.map +1 -1
  130. package/dist/server/sessions/sessionEnvironmentFacts.js +48 -0
  131. package/dist/server/sessions/sessionEnvironmentFacts.js.map +1 -0
  132. package/dist/server/sessions/sessionFileFormat.js +27 -0
  133. package/dist/server/sessions/sessionFileFormat.js.map +1 -0
  134. package/dist/server/sessions/sessionFileHeader.js +1 -14
  135. package/dist/server/sessions/sessionFileHeader.js.map +1 -1
  136. package/dist/server/sessions/sessionModelScope.js +160 -0
  137. package/dist/server/sessions/sessionModelScope.js.map +1 -0
  138. package/dist/server/sessions/sessionNameGenerator.js +6 -6
  139. package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
  140. package/dist/server/sessions/sessionRoutes.js +55 -0
  141. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  142. package/dist/server/sessions/sessionSummaryScanner.js +96 -224
  143. package/dist/server/sessions/sessionSummaryScanner.js.map +1 -1
  144. package/dist/server/sessions/spawnSubsessionTool.js +5 -9
  145. package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
  146. package/dist/server/sessions/spawnTargetResolver.js +1 -1
  147. package/dist/server/status/machineStatusRoutes.js +8 -0
  148. package/dist/server/status/machineStatusRoutes.js.map +1 -0
  149. package/dist/server/status/machineStatusService.js +181 -0
  150. package/dist/server/status/machineStatusService.js.map +1 -0
  151. package/dist/server/status/workspaceAttribution.js +87 -0
  152. package/dist/server/status/workspaceAttribution.js.map +1 -0
  153. package/dist/server/storage/piPackageDismissalStore.js +78 -0
  154. package/dist/server/storage/piPackageDismissalStore.js.map +1 -0
  155. package/dist/server/terminalProxyRoutes.js +2 -1
  156. package/dist/server/terminalProxyRoutes.js.map +1 -1
  157. package/dist/server/workspaceExplorerRoutes.js +20 -13
  158. package/dist/server/workspaceExplorerRoutes.js.map +1 -1
  159. package/dist/server/workspaces/fileContentService.js +18 -9
  160. package/dist/server/workspaces/fileContentService.js.map +1 -1
  161. package/dist/server/workspaces/filePreviewResponseHeaders.js +18 -0
  162. package/dist/server/workspaces/filePreviewResponseHeaders.js.map +1 -0
  163. package/dist/server/workspaces/filePreviewResponsePolicy.js +55 -0
  164. package/dist/server/workspaces/filePreviewResponsePolicy.js.map +1 -0
  165. package/dist/server/workspaces/filePreviewService.js +81 -0
  166. package/dist/server/workspaces/filePreviewService.js.map +1 -0
  167. package/dist/server/workspaces/projectWorkspaceCwds.js +0 -5
  168. package/dist/server/workspaces/projectWorkspaceCwds.js.map +1 -1
  169. package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js +402 -0
  170. package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js.map +1 -0
  171. package/dist/server/workspaces/workspaceCatalog.js +47 -0
  172. package/dist/server/workspaces/workspaceCatalog.js.map +1 -0
  173. package/dist/server/workspaces/workspaceContext.js +1 -3
  174. package/dist/server/workspaces/workspaceContext.js.map +1 -1
  175. package/dist/server/workspaces/workspaceDeletionRoutes.js +35 -125
  176. package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -1
  177. package/dist/server/workspaces/workspaceProviderRegistry.js +651 -0
  178. package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -0
  179. package/dist/server/workspaces/workspaceRemovalService.js +256 -0
  180. package/dist/server/workspaces/workspaceRemovalService.js.map +1 -0
  181. package/dist/server/workspaces/workspaceRouteErrors.js +7 -0
  182. package/dist/server/workspaces/workspaceRouteErrors.js.map +1 -0
  183. package/dist/server/workspaces/worktreePreRemoveHook.js +39 -0
  184. package/dist/server/workspaces/worktreePreRemoveHook.js.map +1 -0
  185. package/dist/server-plugin-api.d.ts +140 -0
  186. package/dist/server-plugin-api.js +2 -0
  187. package/dist/server-plugin-api.js.map +1 -0
  188. package/dist/serverPluginRecovery.js +138 -0
  189. package/dist/serverPluginRecovery.js.map +1 -0
  190. package/dist/sessiond/activeAgentProfile.js +3 -22
  191. package/dist/sessiond/activeAgentProfile.js.map +1 -1
  192. package/dist/sessiond/config.js +11 -0
  193. package/dist/sessiond/config.js.map +1 -1
  194. package/dist/sessiond/sessionDaemonClient.js +11 -9
  195. package/dist/sessiond/sessionDaemonClient.js.map +1 -1
  196. package/dist/shared/activeAgentProfile.js +2 -41
  197. package/dist/shared/activeAgentProfile.js.map +1 -1
  198. package/dist/shared/activity.js +0 -3
  199. package/dist/shared/activity.js.map +1 -1
  200. package/dist/shared/apiTypes.js +7 -5
  201. package/dist/shared/apiTypes.js.map +1 -1
  202. package/dist/shared/capabilities.js +8 -6
  203. package/dist/shared/capabilities.js.map +1 -1
  204. package/dist/shared/federatedRoutes.js +36 -5
  205. package/dist/shared/federatedRoutes.js.map +1 -1
  206. package/dist/shared/machinePluginIds.js +4 -2
  207. package/dist/shared/machinePluginIds.js.map +1 -1
  208. package/dist/shared/machineStatus.js +94 -0
  209. package/dist/shared/machineStatus.js.map +1 -0
  210. package/dist/shared/piWebStatusParsing.js +33 -0
  211. package/dist/shared/piWebStatusParsing.js.map +1 -1
  212. package/dist/shared/pluginApiTypes.d.ts +150 -0
  213. package/dist/shared/pluginApiTypes.js +3 -0
  214. package/dist/shared/pluginApiTypes.js.map +1 -0
  215. package/dist/shared/pluginBackendProtocol.js +110 -0
  216. package/dist/shared/pluginBackendProtocol.js.map +1 -0
  217. package/dist/shared/pluginIds.js +5 -0
  218. package/dist/shared/pluginIds.js.map +1 -1
  219. package/dist/shared/pluginRecoveryCommands.js +14 -0
  220. package/dist/shared/pluginRecoveryCommands.js.map +1 -0
  221. package/dist/shared/workspaceFiles.js +41 -2
  222. package/dist/shared/workspaceFiles.js.map +1 -1
  223. package/dist/shared/workspaceRemovalProtocol.js +24 -0
  224. package/dist/shared/workspaceRemovalProtocol.js.map +1 -0
  225. package/docs/config.md +105 -52
  226. package/docs/plugins.md +403 -146
  227. package/examples/workspace-provider-plugin/README.md +52 -0
  228. package/examples/workspace-provider-plugin/package.json +25 -0
  229. package/examples/workspace-provider-plugin/src/browser/index.ts +69 -0
  230. package/examples/workspace-provider-plugin/src/server.ts +64 -0
  231. package/examples/workspace-provider-plugin/tsconfig.json +21 -0
  232. package/package.json +30 -11
  233. package/server-plugin-api.d.ts +1 -0
  234. package/dist/client/assets/CodeViewer-D91Sp61M.js +0 -4
  235. package/dist/client/assets/UnifiedDiffViewer-BafLWGtF.js +0 -39
  236. package/dist/client/assets/index-CA8q9_o7.js +0 -4025
  237. package/dist/client/assets/vendor-editor-core-vUi74DnD.js +0 -12
  238. package/dist/client/assets/vendor-editor-languages-DoaXUodB.js +0 -46
  239. package/dist/pi-web-plugins/relays/package.json +0 -9
  240. package/dist/plugin-api/unstable.d.ts +0 -22
  241. package/dist/server/activity/workspaceActivityRoutes.js +0 -4
  242. package/dist/server/activity/workspaceActivityRoutes.js.map +0 -1
  243. package/dist/server/git/gitService.js.map +0 -1
  244. package/dist/server/gitRoutes.js +0 -23
  245. package/dist/server/gitRoutes.js.map +0 -1
  246. package/dist/server/sessions/parentSessionLocator.js +0 -75
  247. package/dist/server/sessions/parentSessionLocator.js.map +0 -1
  248. package/dist/server/workspaces/gitWorktreeDiscovery.js +0 -43
  249. package/dist/server/workspaces/gitWorktreeDiscovery.js.map +0 -1
  250. package/dist/server/workspaces/imagePreviewService.js +0 -40
  251. package/dist/server/workspaces/imagePreviewService.js.map +0 -1
  252. package/dist/server/workspaces/workspaceService.js +0 -52
  253. package/dist/server/workspaces/workspaceService.js.map +0 -1
  254. package/dist/shared/apiTypes.d.ts +0 -1217
  255. package/dist/shared/thinkingLevels.d.ts +0 -27
  256. package/plugin-api/unstable.d.ts +0 -1
  257. /package/dist/{pi-web-plugins → pi-packages}/relays/vendor/README.md +0 -0
  258. /package/dist/{pi-web-plugins → pi-packages}/relays/vendor/marked.esm.js +0 -0
@@ -0,0 +1,609 @@
1
+ // Generated from pi-web-plugins/git/browser/git-panel.ts. Do not edit directly.
2
+ import { GIT_DIFF_OPERATION, GIT_STATUS_OPERATION, parseGitDiffResponse, parseGitStatusResponse, } from "./git-contract.js";
3
+ import { buildGitFileList } from "./gitFileList.js";
4
+ import { buildGitFileTree, collectGitFileTreeDirectoryPaths } from "./gitFileTree.js";
5
+ import { readGitFileView, writeGitFileView } from "./gitFileViewPreference.js";
6
+ import { createGitDiffRoute } from "./gitRoute.js";
7
+ import { parseUnifiedDiff } from "./unifiedDiff.js";
8
+ const GIT_PANEL_LOCAL_ID = "workspace.git";
9
+ const GIT_POLL_INTERVAL_MS = 8_000;
10
+ // Keep navigation state for a few recent workspaces; heavy diff views are
11
+ // released as soon as another machine/workspace becomes active.
12
+ const GIT_WORKSPACE_STATE_LIMIT = 8;
13
+ const activityElementTag = "pi-web-git-panel-activity";
14
+ const EMPTY_LIST_MODEL = { submodules: [], files: [] };
15
+ const EMPTY_VIEW_STATE = { nodes: [], listModel: EMPTY_LIST_MODEL, expandablePaths: [] };
16
+ export function createGitBrowserContributions(sourcePluginId, runtimePluginId, html, svg) {
17
+ const panelId = `${runtimePluginId}:${GIT_PANEL_LOCAL_ID}`;
18
+ const controller = new GitUiController(sourcePluginId, createGitDiffRoute(panelId));
19
+ defineGitPanelActivityElement();
20
+ return {
21
+ actions: createGitActions(panelId, controller),
22
+ workspacePanels: [createGitPanel(html, svg, controller)],
23
+ };
24
+ }
25
+ class GitUiController {
26
+ sourcePluginId;
27
+ route;
28
+ states = new Map();
29
+ activeWorkspaceKey;
30
+ connectedWorkspaceKey;
31
+ routeNavigationPending = true;
32
+ view = readGitFileView();
33
+ constructor(sourcePluginId, route) {
34
+ this.sourcePluginId = sourcePluginId;
35
+ this.route = route;
36
+ }
37
+ isOwnedWorkspace(workspace) {
38
+ return workspace?.provider?.pluginId === this.sourcePluginId;
39
+ }
40
+ state(context) {
41
+ return this.stateFor(context);
42
+ }
43
+ connect(context) {
44
+ const key = workspaceContextKey(context);
45
+ const changedWorkspace = this.activeWorkspaceKey !== key;
46
+ if (changedWorkspace)
47
+ this.releaseInactiveDiff();
48
+ const state = this.stateFor(context);
49
+ this.activeWorkspaceKey = key;
50
+ this.connectedWorkspaceKey = key;
51
+ this.synchronizeRoute(state, changedWorkspace);
52
+ if (state.status === undefined && state.statusRequest === undefined)
53
+ void this.refresh(context);
54
+ else if (state.selectedDiffPath !== undefined && (state.selectedDiff === undefined || state.selectedStagedDiff === undefined) && !state.diffLoading) {
55
+ if (state.status?.files.some((file) => file.path === state.selectedDiffPath) === true)
56
+ void this.refreshDiff(state, state.selectedDiffPath, context);
57
+ else
58
+ this.clearSelection(state, true);
59
+ }
60
+ }
61
+ disconnect(context) {
62
+ if (this.connectedWorkspaceKey === workspaceContextKey(context))
63
+ this.connectedWorkspaceKey = undefined;
64
+ }
65
+ handlePopState(context) {
66
+ this.routeNavigationPending = true;
67
+ if (!this.route.matches(context))
68
+ return;
69
+ this.connect(context);
70
+ this.requestRender(this.stateFor(context));
71
+ }
72
+ poll(context) {
73
+ void this.refresh(context);
74
+ }
75
+ invalidate(context) {
76
+ if (!this.isOwnedWorkspace(context.workspace))
77
+ return Promise.resolve();
78
+ const state = this.stateFor(context);
79
+ state.stale = state.status !== undefined;
80
+ this.requestRender(state);
81
+ return this.refresh(context);
82
+ }
83
+ refresh(context) {
84
+ const state = this.stateFor(context);
85
+ if (state.statusRequest !== undefined)
86
+ return state.statusRequest;
87
+ state.statusLoading = true;
88
+ this.requestRender(state);
89
+ const request = requestGitBackend(context, GIT_STATUS_OPERATION, null)
90
+ .then(parseGitStatusResponse)
91
+ .then(async (status) => {
92
+ if (!state.retained)
93
+ return;
94
+ state.status = state.status?.hash === status.hash ? state.status : status;
95
+ state.stale = false;
96
+ state.error = undefined;
97
+ const path = state.selectedDiffPath;
98
+ if (path === undefined)
99
+ return;
100
+ if (!status.files.some((file) => file.path === path))
101
+ this.clearSelection(state, true);
102
+ else if (this.connectedWorkspaceKey === workspaceContextKey(context))
103
+ await this.refreshDiff(state, path, context);
104
+ })
105
+ .catch((error) => {
106
+ if (state.retained)
107
+ state.error = errorMessage(error);
108
+ })
109
+ .finally(() => {
110
+ if (state.statusRequest !== request)
111
+ return;
112
+ state.statusRequest = undefined;
113
+ state.statusLoading = false;
114
+ this.requestRender(state);
115
+ });
116
+ state.statusRequest = request;
117
+ return request;
118
+ }
119
+ selectDiff(context, path) {
120
+ const state = this.stateFor(context);
121
+ state.selectedDiffPath = path;
122
+ state.selectedDiff = undefined;
123
+ state.selectedStagedDiff = undefined;
124
+ state.diffLoading = true;
125
+ state.error = undefined;
126
+ if (this.route.matches(context))
127
+ this.route.write(path);
128
+ this.requestRender(state);
129
+ void this.refreshDiff(state, path, context);
130
+ }
131
+ setView(context, view) {
132
+ if (this.view === view)
133
+ return;
134
+ this.view = view;
135
+ writeGitFileView(view);
136
+ for (const state of this.states.values()) {
137
+ state.expandedDirectories = new Set();
138
+ state.viewStateCache = undefined;
139
+ }
140
+ this.requestRender(this.stateFor(context));
141
+ }
142
+ currentView() {
143
+ return this.view;
144
+ }
145
+ viewState(state) {
146
+ const cached = state.viewStateCache;
147
+ if (cached !== undefined && cached.status === state.status && cached.view === this.view)
148
+ return cached.viewState;
149
+ const viewState = buildViewState(state.status, this.view);
150
+ state.viewStateCache = { status: state.status, view: this.view, viewState };
151
+ return viewState;
152
+ }
153
+ toggleDirectory(context, path) {
154
+ const state = this.stateFor(context);
155
+ const next = new Set(state.expandedDirectories);
156
+ if (next.has(path))
157
+ next.delete(path);
158
+ else
159
+ next.add(path);
160
+ state.expandedDirectories = next;
161
+ this.requestRender(state);
162
+ }
163
+ toggleExpandAll(context, paths, collapse) {
164
+ const state = this.stateFor(context);
165
+ state.expandedDirectories = collapse ? new Set() : new Set(paths);
166
+ this.requestRender(state);
167
+ }
168
+ stateFor(context) {
169
+ const key = workspaceContextKey(context);
170
+ const existing = this.states.get(key);
171
+ if (existing !== undefined) {
172
+ existing.context = context;
173
+ this.states.delete(key);
174
+ this.states.set(key, existing);
175
+ return existing;
176
+ }
177
+ this.evictOldestState();
178
+ const created = {
179
+ context,
180
+ retained: true,
181
+ routeInitialized: false,
182
+ status: undefined,
183
+ statusLoading: false,
184
+ stale: false,
185
+ selectedDiffPath: undefined,
186
+ selectedDiff: undefined,
187
+ selectedStagedDiff: undefined,
188
+ diffLoading: false,
189
+ error: undefined,
190
+ expandedDirectories: new Set(),
191
+ statusRequest: undefined,
192
+ diffRequestSequence: 0,
193
+ viewStateCache: undefined,
194
+ };
195
+ this.states.set(key, created);
196
+ return created;
197
+ }
198
+ releaseInactiveDiff() {
199
+ if (this.activeWorkspaceKey === undefined)
200
+ return;
201
+ const state = this.states.get(this.activeWorkspaceKey);
202
+ if (state === undefined)
203
+ return;
204
+ state.selectedDiff = undefined;
205
+ state.selectedStagedDiff = undefined;
206
+ state.diffLoading = false;
207
+ state.diffRequestSequence += 1;
208
+ }
209
+ evictOldestState() {
210
+ if (this.states.size < GIT_WORKSPACE_STATE_LIMIT)
211
+ return;
212
+ const key = [...this.states.keys()].find((candidate) => candidate !== this.connectedWorkspaceKey) ?? this.states.keys().next().value;
213
+ if (key === undefined)
214
+ return;
215
+ const state = this.states.get(key);
216
+ if (state !== undefined) {
217
+ state.retained = false;
218
+ state.diffRequestSequence += 1;
219
+ }
220
+ this.states.delete(key);
221
+ }
222
+ synchronizeRoute(state, changedWorkspace) {
223
+ if (!this.route.matches(state.context))
224
+ return;
225
+ const routePath = this.route.read();
226
+ if (this.routeNavigationPending || !state.routeInitialized || (!changedWorkspace && routePath !== state.selectedDiffPath)) {
227
+ this.routeNavigationPending = false;
228
+ state.routeInitialized = true;
229
+ this.applyRouteSelection(state, routePath);
230
+ this.route.write(routePath, { replace: true });
231
+ return;
232
+ }
233
+ if (changedWorkspace)
234
+ this.route.write(state.selectedDiffPath, { replace: true });
235
+ }
236
+ applyRouteSelection(state, path) {
237
+ if (state.selectedDiffPath === path)
238
+ return;
239
+ state.selectedDiffPath = path;
240
+ state.selectedDiff = undefined;
241
+ state.selectedStagedDiff = undefined;
242
+ state.diffLoading = false;
243
+ state.diffRequestSequence += 1;
244
+ }
245
+ clearSelection(state, replaceUrl) {
246
+ this.applyRouteSelection(state, undefined);
247
+ if (replaceUrl && this.connectedWorkspaceKey === workspaceContextKey(state.context) && this.route.matches(state.context)) {
248
+ this.route.write(undefined, { replace: true });
249
+ }
250
+ }
251
+ async refreshDiff(state, path, context) {
252
+ const sequence = state.diffRequestSequence + 1;
253
+ state.diffRequestSequence = sequence;
254
+ state.diffLoading = true;
255
+ this.requestRender(state);
256
+ try {
257
+ const [selectedDiff, selectedStagedDiff] = await Promise.all([
258
+ requestGitBackend(context, GIT_DIFF_OPERATION, { path }).then(parseGitDiffResponse),
259
+ requestGitBackend(context, GIT_DIFF_OPERATION, { path, staged: true }).then(parseGitDiffResponse),
260
+ ]);
261
+ if (!state.retained || state.diffRequestSequence !== sequence || state.selectedDiffPath !== path)
262
+ return;
263
+ state.selectedDiff = createDiffView(selectedDiff, state.selectedDiff);
264
+ state.selectedStagedDiff = createDiffView(selectedStagedDiff, state.selectedStagedDiff);
265
+ state.error = undefined;
266
+ }
267
+ catch (error) {
268
+ if (!state.retained || state.diffRequestSequence !== sequence || state.selectedDiffPath !== path)
269
+ return;
270
+ state.error = errorMessage(error);
271
+ }
272
+ finally {
273
+ if (state.retained && state.diffRequestSequence === sequence && state.selectedDiffPath === path) {
274
+ state.diffLoading = false;
275
+ this.requestRender(state);
276
+ }
277
+ }
278
+ }
279
+ requestRender(state) {
280
+ if (state.retained)
281
+ state.context.host.requestRender();
282
+ }
283
+ }
284
+ function createGitActions(panelId, controller) {
285
+ const hasGitWorkspace = (context) => controller.isOwnedWorkspace(context.state.selectedWorkspace);
286
+ return [
287
+ {
288
+ id: "view.git",
289
+ title: "Go to Git",
290
+ shortcut: "mod+3",
291
+ shortcutAliases: ["core:view.git"],
292
+ group: "Navigation",
293
+ enabled: hasGitWorkspace,
294
+ run: (context) => { context.selectMainView(panelId); },
295
+ },
296
+ {
297
+ id: "workspace.refresh-git",
298
+ title: "Refresh Git",
299
+ shortcut: "mod+shift+g",
300
+ shortcutAliases: ["core:workspace.refresh-git"],
301
+ group: "Workspace",
302
+ enabled: hasGitWorkspace,
303
+ run: (context) => context.refreshWorkspacePanels(panelId),
304
+ },
305
+ ];
306
+ }
307
+ function createGitPanel(html, svg, controller) {
308
+ return {
309
+ id: GIT_PANEL_LOCAL_ID,
310
+ title: "Git",
311
+ icon: svg `
312
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
313
+ <circle cx="6" cy="6" r="2"></circle>
314
+ <circle cx="18" cy="6" r="2"></circle>
315
+ <circle cx="12" cy="18" r="2"></circle>
316
+ <path d="M8 6h6"></path>
317
+ <path d="M6 8v2a6 6 0 0 0 6 6"></path>
318
+ <path d="M18 8v2a6 6 0 0 1-6 6"></path>
319
+ </svg>
320
+ `,
321
+ order: 20,
322
+ routeAliases: ["git", "core:workspace.git"],
323
+ visible: (context) => controller.isOwnedWorkspace(context.workspace),
324
+ onInvalidate: (context) => controller.invalidate(context),
325
+ render: (context) => renderGitPanel(html, controller, context),
326
+ };
327
+ }
328
+ function requestGitBackend(context, operation, input) {
329
+ if (context.backend === undefined || context.workspace.provider?.capabilities.request === false) {
330
+ return Promise.reject(new Error("Git workspace backend is unavailable. Update and restart PI WEB on this machine, then reload the browser."));
331
+ }
332
+ return context.backend.request(operation, input);
333
+ }
334
+ function renderGitPanel(html, controller, context) {
335
+ const state = controller.state(context);
336
+ const viewState = controller.viewState(state);
337
+ return html `
338
+ <section class="git-panel">
339
+ <style .textContent=${gitPanelStyles}></style>
340
+ <pi-web-git-panel-activity .controller=${controller} .context=${context}></pi-web-git-panel-activity>
341
+ <section class="git-toolbar">
342
+ <strong>Git</strong>
343
+ ${state.stale ? html `<span class="git-stale">stale</span>` : null}
344
+ <div class="git-toolbar-actions">
345
+ ${viewState.expandablePaths.length === 0 ? null : renderExpandCollapseAll(html, controller, context, state, viewState.expandablePaths)}
346
+ ${renderViewToggle(html, controller, context)}
347
+ <button type="button" ?disabled=${state.statusLoading} @click=${() => { void controller.refresh(context); }}>Refresh</button>
348
+ </div>
349
+ </section>
350
+ ${state.error === undefined ? null : html `<div class="git-error" role="alert">${state.error}</div>`}
351
+ <section class="git-split">
352
+ <div class="git-file-list">${renderFileList(html, controller, context, state, viewState)}</div>
353
+ <div class="git-viewer">${renderDiffViewer(html, state)}</div>
354
+ </section>
355
+ </section>
356
+ `;
357
+ }
358
+ function renderViewToggle(html, controller, context) {
359
+ return html `
360
+ <div class="git-view-toggle" role="group" aria-label="Changed files view">
361
+ ${renderViewToggleButton(html, controller, context, "list", "List")}
362
+ ${renderViewToggleButton(html, controller, context, "tree", "Tree")}
363
+ </div>
364
+ `;
365
+ }
366
+ function renderViewToggleButton(html, controller, context, view, label) {
367
+ const active = controller.currentView() === view;
368
+ return html `<button type="button" class=${active ? "is-selected" : ""} aria-pressed=${String(active)} @click=${() => { controller.setView(context, view); }}>${label}</button>`;
369
+ }
370
+ function renderExpandCollapseAll(html, controller, context, state, expandablePaths) {
371
+ const allExpanded = expandablePaths.every((path) => state.expandedDirectories.has(path));
372
+ return html `<button type="button" @click=${() => { controller.toggleExpandAll(context, expandablePaths, allExpanded); }}>${allExpanded ? "Collapse all" : "Expand all"}</button>`;
373
+ }
374
+ function renderFileList(html, controller, context, state, viewState) {
375
+ const status = state.status;
376
+ if (status === undefined)
377
+ return html `<p class="git-muted">${state.error === undefined ? "Loading status…" : "Status unavailable."}</p>`;
378
+ if (!status.isGitRepo)
379
+ return html `<p class="git-muted">Not a git repository.</p>`;
380
+ const summary = html `<p class="git-summary">${gitSummary(status)}</p>`;
381
+ if (status.files.length === 0)
382
+ return html `${summary}<p class="git-muted">No changes.</p>`;
383
+ const body = controller.currentView() === "tree"
384
+ ? viewState.nodes.map((node) => renderTreeNode(html, controller, context, state, node, 0))
385
+ : renderListBody(html, controller, context, state, viewState.listModel);
386
+ return html `${summary}${body}`;
387
+ }
388
+ function renderListBody(html, controller, context, state, model) {
389
+ return html `
390
+ ${model.submodules.map((group) => renderSubmoduleGroup(html, controller, context, state, group))}
391
+ ${model.files.map((file) => renderFileRow(html, controller, context, state, file))}
392
+ `;
393
+ }
394
+ function renderSubmoduleGroup(html, controller, context, state, group) {
395
+ const expanded = state.expandedDirectories.has(group.path);
396
+ return html `
397
+ <button type="button" class="git-row" style="--depth:0" aria-expanded=${String(expanded)} @click=${() => { controller.toggleDirectory(context, group.path); }}>
398
+ <span class="git-twisty">${expanded ? "▾" : "▸"}</span>
399
+ <span>${group.name}${submoduleBadge(html)}</span>
400
+ </button>
401
+ ${expanded ? html `
402
+ ${group.pointer === undefined ? null : renderSelectableRow(html, controller, context, state, group.path, group.pointer.name, group.pointer.file, 1)}
403
+ ${group.files.map((entry) => renderSubmoduleFileRow(html, controller, context, state, entry))}
404
+ ` : null}
405
+ `;
406
+ }
407
+ function renderSubmoduleFileRow(html, controller, context, state, entry) {
408
+ return renderSelectableRow(html, controller, context, state, entry.path, entry.relativePath, entry.file, 1);
409
+ }
410
+ function renderTreeNode(html, controller, context, state, node, depth) {
411
+ if (node.kind === "directory") {
412
+ const expanded = state.expandedDirectories.has(node.path);
413
+ return html `
414
+ <button type="button" class="git-row" style=${`--depth:${String(depth)}`} aria-expanded=${String(expanded)} @click=${() => { controller.toggleDirectory(context, node.path); }}>
415
+ <span class="git-twisty">${expanded ? "▾" : "▸"}</span>
416
+ <span>${node.name}${node.isSubmodule === true ? submoduleBadge(html) : null}</span>
417
+ </button>
418
+ ${expanded ? node.children.map((child) => renderTreeNode(html, controller, context, state, child, depth + 1)) : null}
419
+ `;
420
+ }
421
+ return renderSelectableRow(html, controller, context, state, node.path, node.name, node.file, depth);
422
+ }
423
+ function renderFileRow(html, controller, context, state, file) {
424
+ return renderSelectableRow(html, controller, context, state, file.path, file.path, file, 0);
425
+ }
426
+ function renderSelectableRow(html, controller, context, state, path, label, file, depth) {
427
+ const selected = state.selectedDiffPath === path;
428
+ return html `
429
+ <button type="button" class=${selected ? "git-row is-selected" : "git-row"} style=${`--depth:${String(depth)}`} @click=${() => { controller.selectDiff(context, path); }}>
430
+ <span>${stateLabel(file.index, file.workingTree)}</span>
431
+ <span>${label}</span>
432
+ </button>
433
+ `;
434
+ }
435
+ function renderDiffViewer(html, state) {
436
+ if (state.selectedDiffPath === undefined)
437
+ return html `<p class="git-muted">Select a changed file.</p>`;
438
+ const unstaged = state.selectedDiff;
439
+ const staged = state.selectedStagedDiff;
440
+ if (unstaged === undefined || staged === undefined)
441
+ return html `<p class="git-muted">Loading diff…</p>`;
442
+ const diffs = [staged, unstaged].filter((diff) => diff.response.diff !== "");
443
+ if (diffs.length === 0)
444
+ return html `<p class="git-muted">No staged or unstaged diff.</p>`;
445
+ return html `<div class=${diffs.length === 1 ? "git-diffs is-single" : "git-diffs"}>${diffs.map((diff) => renderDiffSection(html, diff))}</div>`;
446
+ }
447
+ function renderDiffSection(html, view) {
448
+ const diff = view.response;
449
+ const lines = view.lines ??= parseUnifiedDiff(diff.diff);
450
+ return html `
451
+ <section class="git-diff-section">
452
+ <div class="git-viewer-header"><strong>${diff.path ?? "diff"}</strong><small>${diff.staged ? "staged" : "unstaged"}${diff.truncated ? " · truncated" : ""}</small></div>
453
+ ${lines.length === 0 ? html `<p class="git-muted">No diff.</p>` : html `
454
+ <div class="git-diff-scroller">
455
+ <div class="git-diff-grid" role="table" aria-label="Unified diff">
456
+ ${lines.map((line) => renderDiffLine(html, line))}
457
+ </div>
458
+ </div>
459
+ `}
460
+ </section>
461
+ `;
462
+ }
463
+ function renderDiffLine(html, line) {
464
+ return html `
465
+ <div class="git-diff-line" role="row">
466
+ <span class=${`git-diff-cell git-line-number ${line.kind}`} role="cell">${formatLineNumber(line.oldLineNumber)}</span>
467
+ <span class=${`git-diff-cell git-line-number ${line.kind}`} role="cell">${formatLineNumber(line.newLineNumber)}</span>
468
+ <span class=${`git-diff-cell git-prefix ${line.kind}`} role="cell">${line.prefix}</span>
469
+ <span class=${`git-diff-cell git-content ${line.kind}`} role="cell">${renderDiffSpans(html, line.spans)}</span>
470
+ </div>
471
+ `;
472
+ }
473
+ function renderDiffSpans(html, spans) {
474
+ return spans.map((span) => html `<span class=${span.changed ? "inline-change" : ""}>${span.text}</span>`);
475
+ }
476
+ function buildViewState(status, view) {
477
+ if (status === undefined || !status.isGitRepo || status.files.length === 0)
478
+ return EMPTY_VIEW_STATE;
479
+ if (view === "tree") {
480
+ const nodes = buildGitFileTree(status.files, status.submodules);
481
+ return { nodes, listModel: EMPTY_LIST_MODEL, expandablePaths: collectGitFileTreeDirectoryPaths(nodes) };
482
+ }
483
+ const listModel = buildGitFileList(status.files, status.submodules);
484
+ return { nodes: [], listModel, expandablePaths: listModel.submodules.map((group) => group.path) };
485
+ }
486
+ function defineGitPanelActivityElement() {
487
+ if (typeof customElements === "undefined" || typeof HTMLElement === "undefined" || customElements.get(activityElementTag) !== undefined)
488
+ return;
489
+ class GitPanelActivityElement extends HTMLElement {
490
+ controllerValue;
491
+ contextValue;
492
+ pollTimer;
493
+ set controller(value) {
494
+ if (this.controllerValue === value)
495
+ return;
496
+ this.controllerValue = value;
497
+ this.restart();
498
+ }
499
+ set context(value) {
500
+ const previousKey = this.contextValue === undefined ? undefined : workspaceContextKey(this.contextValue);
501
+ this.contextValue = value;
502
+ if (previousKey !== (value === undefined ? undefined : workspaceContextKey(value)))
503
+ this.restart();
504
+ }
505
+ connectedCallback() {
506
+ window.addEventListener("popstate", this.onPopState);
507
+ this.restart();
508
+ }
509
+ disconnectedCallback() {
510
+ window.removeEventListener("popstate", this.onPopState);
511
+ if (this.controllerValue !== undefined && this.contextValue !== undefined)
512
+ this.controllerValue.disconnect(this.contextValue);
513
+ this.stopTimer();
514
+ }
515
+ restart() {
516
+ this.stopTimer();
517
+ if (!this.isConnected || this.controllerValue === undefined || this.contextValue === undefined)
518
+ return;
519
+ this.controllerValue.connect(this.contextValue);
520
+ this.pollTimer = window.setInterval(() => {
521
+ if (this.controllerValue !== undefined && this.contextValue !== undefined)
522
+ this.controllerValue.poll(this.contextValue);
523
+ }, GIT_POLL_INTERVAL_MS);
524
+ }
525
+ stopTimer() {
526
+ if (this.pollTimer !== undefined)
527
+ window.clearInterval(this.pollTimer);
528
+ this.pollTimer = undefined;
529
+ }
530
+ onPopState = () => {
531
+ if (this.controllerValue !== undefined && this.contextValue !== undefined)
532
+ this.controllerValue.handlePopState(this.contextValue);
533
+ };
534
+ }
535
+ customElements.define(activityElementTag, GitPanelActivityElement);
536
+ }
537
+ function submoduleBadge(html) {
538
+ return html `<span class="submodule-badge">submodule</span>`;
539
+ }
540
+ function gitSummary(status) {
541
+ const branch = status.branch ?? "detached";
542
+ const ahead = status.ahead ?? 0;
543
+ const behind = status.behind ?? 0;
544
+ return ahead === 0 && behind === 0 ? branch : `${branch} · ↑${String(ahead)} ↓${String(behind)}`;
545
+ }
546
+ function stateLabel(index, workingTree) {
547
+ const label = workingTree !== "unmodified" ? workingTree : index;
548
+ return label.slice(0, 1).toUpperCase();
549
+ }
550
+ function formatLineNumber(lineNumber) {
551
+ return lineNumber === undefined ? "" : String(lineNumber);
552
+ }
553
+ function createDiffView(response, previous) {
554
+ return {
555
+ response,
556
+ lines: previous?.response.hash === response.hash ? previous.lines : undefined,
557
+ };
558
+ }
559
+ function workspaceContextKey(context) {
560
+ return JSON.stringify([context.machine.id, context.workspace.projectId, context.workspace.id]);
561
+ }
562
+ function errorMessage(error) {
563
+ return error instanceof Error ? error.message : String(error);
564
+ }
565
+ const gitPanelStyles = `
566
+ .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; }
567
+ .git-panel ${activityElementTag} { display: none; }
568
+ .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; }
569
+ .git-panel button:disabled { cursor: wait; opacity: .65; }
570
+ .git-panel small, .git-panel .git-muted { color: var(--pi-muted); }
571
+ .git-panel p { margin: 10px; }
572
+ .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); }
573
+ .git-panel .git-toolbar-actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }
574
+ .git-panel .git-view-toggle { display: inline-flex; }
575
+ .git-panel .git-view-toggle button { border-radius: 0; }
576
+ .git-panel .git-view-toggle button:first-child { border-top-left-radius: 7px; border-bottom-left-radius: 7px; }
577
+ .git-panel .git-view-toggle button:last-child { margin-left: -1px; border-top-right-radius: 7px; border-bottom-right-radius: 7px; }
578
+ .git-panel .git-view-toggle button.is-selected { position: relative; z-index: 1; border-color: var(--pi-accent); background: var(--pi-selection-bg); }
579
+ .git-panel .git-stale { border: 1px solid var(--pi-warning-border); border-radius: 999px; color: var(--pi-warning); padding: 1px 6px; font-size: 12px; }
580
+ .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; }
581
+ .git-panel .git-split { flex: 1 1 auto; min-height: 0; display: grid; grid-template-rows: minmax(160px, 34%) minmax(0, 1fr); }
582
+ .git-panel .git-file-list { min-height: 0; overflow: auto; border-bottom: 1px solid var(--pi-border); padding: 6px; }
583
+ .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); }
584
+ .git-panel .git-row:hover, .git-panel .git-row.is-selected { background: var(--pi-selection-bg); }
585
+ .git-panel .git-row span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
586
+ .git-panel .git-twisty { color: var(--pi-dim, var(--pi-muted)); }
587
+ .git-panel .git-summary { margin: 4px 6px 8px; color: var(--pi-muted); }
588
+ .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; }
589
+ .git-panel .git-viewer { min-height: 0; overflow: auto; display: flex; flex-direction: column; }
590
+ .git-panel .git-diffs { flex: 1 1 auto; min-height: 0; overflow: auto; display: grid; grid-template-rows: minmax(120px, 1fr) minmax(120px, 1fr); }
591
+ .git-panel .git-diffs.is-single { grid-template-rows: minmax(0, 1fr); }
592
+ .git-panel .git-diff-section { min-height: 0; display: flex; flex-direction: column; border-bottom: 1px solid var(--pi-border); }
593
+ .git-panel .git-diff-section:last-child { border-bottom: 0; }
594
+ .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); }
595
+ .git-panel .git-viewer-header strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
596
+ .git-panel .git-diff-scroller { flex: 1 1 auto; min-height: 0; overflow: auto; background: var(--pi-bg); }
597
+ .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; }
598
+ .git-panel .git-diff-line { display: contents; }
599
+ .git-panel .git-diff-cell { min-height: 1.45em; white-space: pre; }
600
+ .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; }
601
+ .git-panel .git-prefix { padding: 0 4px; color: var(--pi-dim); text-align: center; user-select: none; }
602
+ .git-panel .git-content { padding: 0 12px 0 4px; }
603
+ .git-panel .git-diff-cell.meta, .git-panel .git-diff-cell.marker { color: var(--pi-dim); }
604
+ .git-panel .git-diff-cell.hunk { background: color-mix(in srgb, var(--pi-accent) 9%, transparent); color: var(--pi-accent); }
605
+ .git-panel .git-diff-cell.add { background: color-mix(in srgb, var(--pi-success) 12%, transparent); }
606
+ .git-panel .git-diff-cell.remove { background: color-mix(in srgb, var(--pi-danger) 12%, transparent); }
607
+ .git-panel .git-content.add .inline-change { border-radius: 2px; background: color-mix(in srgb, var(--pi-success) 36%, transparent); color: var(--pi-text); }
608
+ .git-panel .git-content.remove .inline-change { border-radius: 2px; background: color-mix(in srgb, var(--pi-danger) 36%, transparent); color: var(--pi-text); }
609
+ `;
@@ -0,0 +1,43 @@
1
+ // Generated from pi-web-plugins/git/browser/gitFileList.ts. Do not edit directly.
2
+ import { pointerName, segmentName } from "./gitFileShared.js";
3
+ /**
4
+ * Group the flat changed-file list for list view. Unlike the tree, submodule
5
+ * contents are flattened: each submodule becomes one expandable group holding
6
+ * its pointer row and its changed files, and everything else stays a flat list.
7
+ */
8
+ export function buildGitFileList(files, submodules = []) {
9
+ const submoduleSet = new Set(submodules);
10
+ const pointers = new Map();
11
+ const grouped = new Map();
12
+ for (const submodule of submodules)
13
+ grouped.set(submodule, []);
14
+ const flat = [];
15
+ for (const file of files) {
16
+ if (submoduleSet.has(file.path)) {
17
+ pointers.set(file.path, { name: pointerName(file), file });
18
+ continue;
19
+ }
20
+ const owner = ownerSubmodule(file.path, submodules);
21
+ if (owner !== undefined) {
22
+ grouped.get(owner)?.push({ path: file.path, relativePath: file.path.slice(owner.length + 1), file });
23
+ continue;
24
+ }
25
+ flat.push(file);
26
+ }
27
+ const groups = submodules
28
+ .map((submodule) => {
29
+ const pointer = pointers.get(submodule);
30
+ const inner = grouped.get(submodule) ?? [];
31
+ return { path: submodule, name: segmentName(submodule), ...(pointer === undefined ? {} : { pointer }), files: [...inner].sort((left, right) => left.relativePath.localeCompare(right.relativePath)) };
32
+ })
33
+ .sort((left, right) => left.name.localeCompare(right.name));
34
+ return { submodules: groups, files: flat };
35
+ }
36
+ function ownerSubmodule(path, submodules) {
37
+ let best;
38
+ for (const submodule of submodules) {
39
+ if (submodule !== "" && path.startsWith(`${submodule}/`) && (best === undefined || submodule.length > best.length))
40
+ best = submodule;
41
+ }
42
+ return best;
43
+ }
@@ -0,0 +1,16 @@
1
+ // Generated from pi-web-plugins/git/browser/gitFileShared.ts. Do not edit directly.
2
+ /**
3
+ * Display label for a submodule commit-pointer row: the `<old> → <new>`
4
+ * short-SHA summary, or "commit" when the server did not resolve both ends.
5
+ */
6
+ export function pointerName(file) {
7
+ const from = file.submoduleFromCommit;
8
+ const to = file.submoduleToCommit;
9
+ return from !== undefined && to !== undefined ? `${from} → ${to}` : "commit";
10
+ }
11
+ /** Final segment of a path, used as the display name for tree/list rows. */
12
+ export function segmentName(path) {
13
+ const segments = path.split("/");
14
+ const last = segments[segments.length - 1];
15
+ return last ?? path;
16
+ }