@jmfederico/pi-web 1.202610.1 → 1.202610.3

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 (148) hide show
  1. package/README.md +23 -134
  2. package/dist/cli.js +4 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/client/assets/index-BAl5IbYx.js +4429 -0
  5. package/dist/client/assets/vendor-editor-core-C2PJX3uW.js +10 -0
  6. package/dist/client/assets/{vendor-editor-languages-DOCw3vc9.js → vendor-editor-languages-CS6n1J8n.js} +1 -1
  7. package/dist/client/index.html +4 -48
  8. package/dist/config.js +8 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/pi-packages/captains-log/dist/browser/index.js +30 -32
  11. package/dist/pi-packages/captains-log/dist/companion.js +1 -1
  12. package/dist/pi-packages/captains-log/package.json +4 -4
  13. package/dist/pi-packages/relays/extensions/index.js +60 -0
  14. package/dist/pi-packages/relays/package.json +10 -1
  15. package/dist/pi-packages/relays/pi-web-plugin.js +31 -1
  16. package/dist/pi-packages/relays/prompts/relay-worktree.md +6 -16
  17. package/dist/pi-packages/relays/prompts/relay.md +6 -33
  18. package/dist/pi-packages/relays/relayDispatch.js +67 -0
  19. package/dist/pi-packages/relays/relayIdentity.js +37 -0
  20. package/dist/pi-packages/relays/relaysPanelElement.js +37 -12
  21. package/dist/pi-packages/relays/skills/relay/SKILL.md +24 -68
  22. package/dist/pi-packages/relays/skills/relay-runner/SKILL.md +40 -219
  23. package/dist/pi-packages/workflows/browser/pi-web-plugin.js +33 -0
  24. package/dist/pi-packages/workflows/package.json +22 -0
  25. package/dist/pi-packages/workflows/prompts/worktree.md +12 -0
  26. package/dist/pi-web-plugins/files/browser/pi-web-plugin.js +49 -51
  27. package/dist/pi-web-plugins/git/browser/git-panel.js +0 -1
  28. package/dist/pi-web-plugins/git/server-plugin.js +13 -2
  29. package/dist/pi-web-plugins/git/workspace-discovery-notify.cjs +14 -0
  30. package/dist/pi-web-plugins/git/workspaceDiscoveryHooks.js +186 -0
  31. package/dist/pi-web-plugins/info/infoInternals.js +3 -3
  32. package/dist/pi-web-plugins/info/pi-web-plugin.js +1 -1
  33. package/dist/pi-web-plugins/mermaid/browser/mermaid-engine.js +1087 -422
  34. package/dist/pi-web-plugins/terminal/browser/pi-web-plugin.js +30 -30
  35. package/dist/pi-web-plugins/updates/pi-web-plugin.js +1 -1
  36. package/dist/piWebUpdate.js +2 -2
  37. package/dist/piWebUpdate.js.map +1 -1
  38. package/dist/plugin-api.d.ts +229 -8
  39. package/dist/server/activity/workspaceActivityService.js +4 -1
  40. package/dist/server/activity/workspaceActivityService.js.map +1 -1
  41. package/dist/server/app.js +5 -0
  42. package/dist/server/app.js.map +1 -1
  43. package/dist/server/configRoutes.js +6 -0
  44. package/dist/server/configRoutes.js.map +1 -1
  45. package/dist/server/knownAutoInstallPiPackages.js +6 -0
  46. package/dist/server/knownAutoInstallPiPackages.js.map +1 -1
  47. package/dist/server/navigationPreferenceRoutes.js +39 -0
  48. package/dist/server/navigationPreferenceRoutes.js.map +1 -0
  49. package/dist/server/plugins/projectPluginSettings.js +95 -0
  50. package/dist/server/plugins/projectPluginSettings.js.map +1 -0
  51. package/dist/server/plugins/serverPluginRuntime.js +26 -3
  52. package/dist/server/plugins/serverPluginRuntime.js.map +1 -1
  53. package/dist/server/sessiond/autoInstallPiPackages.js +3 -3
  54. package/dist/server/sessiond/workspaceCatalogRoutes.js +3 -1
  55. package/dist/server/sessiond/workspaceCatalogRoutes.js.map +1 -1
  56. package/dist/server/sessiond/workspaceNotificationRoutes.js +41 -0
  57. package/dist/server/sessiond/workspaceNotificationRoutes.js.map +1 -0
  58. package/dist/server/sessiond.js +40 -7
  59. package/dist/server/sessiond.js.map +1 -1
  60. package/dist/server/sessions/askUserDomain.js +232 -0
  61. package/dist/server/sessions/askUserDomain.js.map +1 -0
  62. package/dist/server/sessions/askUserTool.js +15 -17
  63. package/dist/server/sessions/askUserTool.js.map +1 -1
  64. package/dist/server/sessions/dockerEnvironmentFacts.js +8 -0
  65. package/dist/server/sessions/dockerEnvironmentFacts.js.map +1 -1
  66. package/dist/server/sessions/extensionSessionDispatch.js +76 -0
  67. package/dist/server/sessions/extensionSessionDispatch.js.map +1 -0
  68. package/dist/server/sessions/globalProviderPolicy.js +71 -16
  69. package/dist/server/sessions/globalProviderPolicy.js.map +1 -1
  70. package/dist/server/sessions/messageHistoryTarget.js +61 -0
  71. package/dist/server/sessions/messageHistoryTarget.js.map +1 -0
  72. package/dist/server/sessions/piSessionManagerGateway.js +23 -24
  73. package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
  74. package/dist/server/sessions/piSessionService.js +759 -276
  75. package/dist/server/sessions/piSessionService.js.map +1 -1
  76. package/dist/server/sessions/queuedMessageMutation.js +168 -0
  77. package/dist/server/sessions/queuedMessageMutation.js.map +1 -0
  78. package/dist/server/sessions/sessionArchiveStore.js +5 -1
  79. package/dist/server/sessions/sessionArchiveStore.js.map +1 -1
  80. package/dist/server/sessions/sessionArchiveTree.js +2 -2
  81. package/dist/server/sessions/sessionArchiveTree.js.map +1 -1
  82. package/dist/server/sessions/sessionCommandService.js +9 -3
  83. package/dist/server/sessions/sessionCommandService.js.map +1 -1
  84. package/dist/server/sessions/sessionImageDimensions.js +31 -0
  85. package/dist/server/sessions/sessionImageDimensions.js.map +1 -0
  86. package/dist/server/sessions/sessionMediaIndex.js +7 -1
  87. package/dist/server/sessions/sessionMediaIndex.js.map +1 -1
  88. package/dist/server/sessions/sessionMetadata.js +51 -0
  89. package/dist/server/sessions/sessionMetadata.js.map +1 -0
  90. package/dist/server/sessions/sessionNameGenerator.js +8 -28
  91. package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
  92. package/dist/server/sessions/sessionRoutes.js +20 -2
  93. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  94. package/dist/server/sessions/sessionSummaryScanner.js +62 -5
  95. package/dist/server/sessions/sessionSummaryScanner.js.map +1 -1
  96. package/dist/server/sessions/spawnSessionTool.js +3 -1
  97. package/dist/server/sessions/spawnSessionTool.js.map +1 -1
  98. package/dist/server/sessions/spawnSubsessionTool.js +25 -17
  99. package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
  100. package/dist/server/sessions/subsessionCompletionDelivery.js +15 -0
  101. package/dist/server/sessions/subsessionCompletionDelivery.js.map +1 -0
  102. package/dist/server/sessions/subsessionCompletionInbox.js +134 -0
  103. package/dist/server/sessions/subsessionCompletionInbox.js.map +1 -0
  104. package/dist/server/sessions/subsessionProvenance.js +27 -0
  105. package/dist/server/sessions/subsessionProvenance.js.map +1 -0
  106. package/dist/server/status/machineStatusService.js +17 -5
  107. package/dist/server/status/machineStatusService.js.map +1 -1
  108. package/dist/server/storage/navigationPreferenceStore.js +137 -0
  109. package/dist/server/storage/navigationPreferenceStore.js.map +1 -0
  110. package/dist/server/workspaces/projectPiWebConfig.js +88 -6
  111. package/dist/server/workspaces/projectPiWebConfig.js.map +1 -1
  112. package/dist/server/workspaces/workspaceChangeNotifier.js +12 -0
  113. package/dist/server/workspaces/workspaceChangeNotifier.js.map +1 -0
  114. package/dist/server/workspaces/workspaceNotificationCommand.js +64 -0
  115. package/dist/server/workspaces/workspaceNotificationCommand.js.map +1 -0
  116. package/dist/server/workspaces/workspaceProviderRegistry.js +55 -0
  117. package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -1
  118. package/dist/server-plugin-api.d.ts +18 -0
  119. package/dist/server-plugin-api.js.map +1 -1
  120. package/dist/sessiond/sessionDaemonClient.js +3 -3
  121. package/dist/sessiond/sessionDaemonClient.js.map +1 -1
  122. package/dist/shared/apiTypes.js +2 -2
  123. package/dist/shared/apiTypes.js.map +1 -1
  124. package/dist/shared/federatedRoutes.js +6 -0
  125. package/dist/shared/federatedRoutes.js.map +1 -1
  126. package/dist/shared/machineStatus.js +11 -2
  127. package/dist/shared/machineStatus.js.map +1 -1
  128. package/dist/shared/navigationPreferences.js +6 -0
  129. package/dist/shared/navigationPreferences.js.map +1 -0
  130. package/dist/shared/pluginApiTypes.d.ts +2 -0
  131. package/dist/shared/sessionMedia.js +6 -0
  132. package/dist/shared/sessionMedia.js.map +1 -1
  133. package/dist/shared/streamingBehavior.js +7 -0
  134. package/dist/shared/streamingBehavior.js.map +1 -0
  135. package/dist/shared/themePreference.js +23 -0
  136. package/dist/shared/themePreference.js.map +1 -0
  137. package/dist/workspaceNotificationCli.js +95 -0
  138. package/dist/workspaceNotificationCli.js.map +1 -0
  139. package/docs/config.md +111 -86
  140. package/docs/plugins.md +105 -30
  141. package/examples/session-bridge-plugin/package.json +2 -2
  142. package/examples/session-bridge-plugin/src/companion.ts +1 -1
  143. package/examples/workspace-provider-plugin/package.json +1 -1
  144. package/package.json +33 -33
  145. package/dist/client/assets/index-CBjDnBIN.js +0 -4178
  146. package/dist/client/assets/vendor-editor-core-YWog6mAR.js +0 -10
  147. package/dist/server/sessions/pendingAskStore.js +0 -275
  148. package/dist/server/sessions/pendingAskStore.js.map +0 -1
@@ -346,7 +346,6 @@ function createGitPanel(html, svg, controller) {
346
346
  </svg>
347
347
  `,
348
348
  order: 20,
349
- routeAliases: ["git", "core:workspace.git"],
350
349
  visible: (context) => controller.isOwnedWorkspace(context.workspace),
351
350
  onInvalidate: (context) => controller.invalidate(context),
352
351
  render: (context) => renderGitPanel(html, controller, context),
@@ -1,14 +1,23 @@
1
1
  // Generated from pi-web-plugins/git/server-plugin.ts. Do not edit directly.
2
2
  import { basename, resolve } from "node:path";
3
3
  import { requestGitBackend } from "./git-backend.js";
4
+ import { createWorkspaceDiscoveryHooks } from "./workspaceDiscoveryHooks.js";
4
5
  const GIT_LOCAL_ENV_VARS = Object.freeze([
5
6
  "GIT_ALTERNATE_OBJECT_DIRECTORIES",
6
7
  "GIT_COMMON_DIR",
8
+ "GIT_CONFIG",
9
+ "GIT_CONFIG_COUNT",
10
+ "GIT_CONFIG_PARAMETERS",
7
11
  "GIT_DIR",
12
+ "GIT_GRAFT_FILE",
13
+ "GIT_IMPLICIT_WORK_TREE",
8
14
  "GIT_INDEX_FILE",
15
+ "GIT_NO_REPLACE_OBJECTS",
9
16
  "GIT_OBJECT_DIRECTORY",
10
17
  "GIT_PREFIX",
11
18
  "GIT_QUARANTINE_PATH",
19
+ "GIT_REPLACE_REF_BASE",
20
+ "GIT_SHALLOW_FILE",
12
21
  "GIT_WORK_TREE",
13
22
  ]);
14
23
  const plugin = {
@@ -16,15 +25,16 @@ const plugin = {
16
25
  name: "Git",
17
26
  activate(context) {
18
27
  return {
19
- workspaceProvider: createGitWorkspaceProvider(context),
28
+ workspaceProvider: createGitWorkspaceProvider(context, process.env["PI_WEB_INSTANCE_NAMESPACE"] ?? ""),
20
29
  peer: { request: (request) => requestGitBackend(context, request) },
21
30
  };
22
31
  },
23
32
  };
24
33
  export default plugin;
25
- export function createGitWorkspaceProvider(context) {
34
+ export function createGitWorkspaceProvider(context, instanceNamespace = "") {
26
35
  return Object.freeze({
27
36
  fallback: true,
37
+ integration: createWorkspaceDiscoveryHooks(context, (cwd, args, signal) => runGit(context, cwd, args, signal), process.execPath, instanceNamespace),
28
38
  async probe(project, signal) {
29
39
  const result = await runGit(context, project.path, ["rev-parse", "--is-inside-work-tree"], signal);
30
40
  if (result.signal !== null)
@@ -161,6 +171,7 @@ async function runGit(context, cwd, args, signal) {
161
171
  file: "git",
162
172
  args: ["-C", cwd, ...args],
163
173
  unsetEnv: GIT_LOCAL_ENV_VARS,
174
+ timeoutMs: 10_000,
164
175
  signal,
165
176
  });
166
177
  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
+ }
@@ -198,7 +198,7 @@ function renderWorkspaceSection(html, workspace) {
198
198
  </section>
199
199
  `;
200
200
  }
201
- /** Panel body: render the Info tab for the current workspace panel context. */
201
+ /** Panel body: machine status is useful even without a selected workspace. */
202
202
  export function renderInfoPanel(html, context) {
203
203
  return html `
204
204
  <style>
@@ -214,9 +214,9 @@ export function renderInfoPanel(html, context) {
214
214
  </style>
215
215
  <section class="toolbar"><strong>Info</strong></section>
216
216
  <section class="viewer info-status">
217
- ${renderStatusSection(html, context.state?.piWebStatus)}
217
+ ${renderStatusSection(html, context.state.piWebStatus)}
218
218
  ${renderMachineSection(html, context.machine)}
219
- ${renderWorkspaceSection(html, context.workspace)}
219
+ ${context.workspace === undefined ? null : renderWorkspaceSection(html, context.workspace)}
220
220
  </section>
221
221
  `;
222
222
  }
@@ -27,7 +27,7 @@ const plugin = {
27
27
  items: (context) => [{ type: "text", text: context.workspace.provider?.pluginId ?? "folder", title: context.workspace.path }],
28
28
  },
29
29
  ],
30
- workspacePanels: [
30
+ applicationPanels: [
31
31
  {
32
32
  id: "workspace.info",
33
33
  title: "Info",