pi-revit 0.2.5 → 0.2.7

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,33 @@ Format follows [Keep a Changelog](https://keepachangelog.com/); version headers
7
7
  Every published version gets an entry with **Added** / **Changed** / **Fixed** sections
8
8
  describing what the user will notice — not internal refactors.
9
9
 
10
+ ## [0.2.7] - 2026-07-21
11
+
12
+ ### Added
13
+ - Update announcements: after a pi-revit update, the next pi session shows a one-time
14
+ "What's new in pi-revit" note listing the changelog entries for every version since the
15
+ last one announced — the same mechanism pi uses for its own updates. The last-announced
16
+ version is remembered in `%APPDATA%\pi-revit\state.json` (kept outside the package
17
+ folder so npm updates can't erase it). Fresh installs stay silent.
18
+
19
+ Extension-only change: no Revit add-in redeploy or Revit restart needed.
20
+
21
+ ## [0.2.6] - 2026-07-20
22
+
23
+ ### Fixed
24
+ - Write transactions (`set_parameters`, `execute_csharp`, and the temporary-isolate
25
+ branch of `manage_selection`) now register a failures preprocessor. Previously Revit
26
+ handled commit failures interactively: warnings popped the transient toast and spammed
27
+ the journal, and an error-severity failure showed the modal resolution dialog, blocking
28
+ the bridge until a human clicked. Now warnings are auto-dismissed and reported back
29
+ (`commitWarnings` in the result, e.g. duplicate Mark values), and errors roll the
30
+ transaction back with the actual Revit failure text in the error message.
31
+
32
+ ### Changed
33
+ - `set_parameters` tool description tells the model to relay `commitWarnings` to the user.
34
+
35
+ Requires redeploying the Revit add-in (`scripts\deploy.ps1` + Revit restart).
36
+
10
37
  ## [0.2.5] - 2026-07-20
11
38
 
12
39
  ### Fixed
@@ -1,8 +1,9 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type, type TSchema } from "typebox";
3
- import { readFile } from "node:fs/promises";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
6
7
  import { version as packageVersion } from "../../package.json";
7
8
 
8
9
  interface BridgeInfo {
@@ -207,6 +208,73 @@ function versionMismatch(addinVersion: unknown): string | null {
207
208
  return `pi-revit ${packageVersion} is installed, but the Revit add-in ${state} — the update is incomplete. Close Revit and run: npx.cmd -y pi-revit (or ask the agent to run scripts\\deploy.ps1 from the installed package, then restart Revit).`;
208
209
  }
209
210
 
211
+ // ------------------------------------------------------------- what's new
212
+
213
+ /** Stable per-user state file. Deliberately NOT under node_modules: npm wipes
214
+ * the package folder on every update, which is exactly when the last-announced
215
+ * version must survive. */
216
+ function announcerStatePath(): string {
217
+ const appData = process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
218
+ return path.join(appData, "pi-revit", "state.json");
219
+ }
220
+
221
+ function compareVersions(a: string, b: string): number {
222
+ const pa = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
223
+ const pb = b.split(".").map((part) => Number.parseInt(part, 10) || 0);
224
+ for (let i = 0; i < 3; i++) {
225
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
226
+ }
227
+ return 0;
228
+ }
229
+
230
+ /** Parses `## [x.y.z]` entries from the packaged CHANGELOG.md (the same header
231
+ * format pi's own changelog parser reads) and returns the ones newer than
232
+ * sinceVersion, newest first — so a 0.2.5 -> 0.2.9 jump shows all four. */
233
+ async function newChangelogEntries(sinceVersion: string): Promise<{ version: string; body: string }[]> {
234
+ const changelogPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "CHANGELOG.md");
235
+ const markdown = await readFile(changelogPath, "utf8");
236
+ const headers = [...markdown.matchAll(/^##\s+\[?(\d+\.\d+\.\d+)\]?[^\n]*$/gm)];
237
+ const entries: { version: string; body: string }[] = [];
238
+ for (let i = 0; i < headers.length; i++) {
239
+ const version = headers[i][1];
240
+ if (compareVersions(version, sinceVersion) <= 0) continue;
241
+ const start = (headers[i].index ?? 0) + headers[i][0].length;
242
+ const end = i + 1 < headers.length ? headers[i + 1].index : markdown.length;
243
+ entries.push({ version, body: markdown.slice(start, end).trim() });
244
+ }
245
+ return entries;
246
+ }
247
+
248
+ /** Shows the changelog entries between the last announced version and the
249
+ * current one, once per update, then records the current version. A fresh
250
+ * install records silently (nothing is "new" yet). Every failure is swallowed:
251
+ * the announcer must never break a session. */
252
+ async function announceUpdateOnce(notify: (message: string, level: "info") => void): Promise<void> {
253
+ try {
254
+ const statePath = announcerStatePath();
255
+ let lastVersion: string | null = null;
256
+ try {
257
+ const state = JSON.parse(await readFile(statePath, "utf8")) as { lastAnnouncedVersion?: string };
258
+ if (typeof state.lastAnnouncedVersion === "string") lastVersion = state.lastAnnouncedVersion;
259
+ } catch {
260
+ // First run: no state yet.
261
+ }
262
+ if (lastVersion === packageVersion) return;
263
+
264
+ if (lastVersion) {
265
+ const entries = await newChangelogEntries(lastVersion);
266
+ if (entries.length > 0) {
267
+ const text = entries.map((entry) => `pi-revit ${entry.version}\n${entry.body}`).join("\n\n");
268
+ notify(`What's new in pi-revit (updated from ${lastVersion}):\n\n${text}`, "info");
269
+ }
270
+ }
271
+ await mkdir(path.dirname(statePath), { recursive: true });
272
+ await writeFile(statePath, JSON.stringify({ lastAnnouncedVersion: packageVersion }, null, 2), "utf8");
273
+ } catch {
274
+ // Never let the announcer break a session.
275
+ }
276
+ }
277
+
210
278
  function registerPing(pi: ExtensionAPI) {
211
279
  pi.registerTool({
212
280
  name: "ping",
@@ -236,6 +304,7 @@ export default async function revitConnector(pi: ExtensionAPI) {
236
304
  // where the user lands after running `pi update --extensions`. Bridge down at
237
305
  // session start is the normal Revit-closed case: stay quiet.
238
306
  pi.on("session_start", async (_event, ctx) => {
307
+ await announceUpdateOnce((message, level) => ctx.ui.notify(message, level));
239
308
  try {
240
309
  const payload = (await bridgeRequest("/ping", { method: "GET" }, undefined, 3_000)) as { addinVersion?: string };
241
310
  const warning = versionMismatch(payload.addinVersion);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-revit",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Native Pi connector for Autodesk Revit. Run npx.cmd -y pi-revit for the full Windows install.",
5
5
  "author": "Ahmad Altahlawi",
6
6
  "license": "MIT",
@@ -113,6 +113,7 @@ namespace RevitBridge.Tools
113
113
 
114
114
  using var dialogGuard = new DialogGuard(uiapp);
115
115
  using var transaction = new Transaction(doc, "execute_csharp");
116
+ var failureGuard = FailureGuard.Attach(transaction);
116
117
  if (transaction.Start() != TransactionStatus.Started)
117
118
  throw new InvalidOperationException("Unable to start the execute_csharp transaction.");
118
119
 
@@ -142,6 +143,7 @@ namespace RevitBridge.Tools
142
143
  if (transaction.Commit() != TransactionStatus.Committed)
143
144
  throw new InvalidOperationException(
144
145
  "Revit rolled back the execute_csharp transaction during commit (failure processing rejected the changes); no model changes were saved."
146
+ + failureGuard.DescribeErrors()
145
147
  + DescribeDialogs(dialogGuard.Suppressed));
146
148
 
147
149
  stopwatch.Stop();
@@ -154,6 +156,8 @@ namespace RevitBridge.Tools
154
156
  };
155
157
  if (dialogGuard.Suppressed.Count > 0)
156
158
  payload["suppressedDialogs"] = dialogGuard.Suppressed;
159
+ if (failureGuard.Warnings.Count > 0)
160
+ payload["commitWarnings"] = failureGuard.Warnings;
157
161
  return payload;
158
162
  }
159
163
 
@@ -0,0 +1,62 @@
1
+ using Autodesk.Revit.DB;
2
+
3
+ namespace RevitBridge.Tools
4
+ {
5
+ /// <summary>
6
+ /// Deterministic commit-time failure handling for tool-owned transactions. Without a
7
+ /// preprocessor, Revit resolves commit failures interactively: warnings pop the
8
+ /// transient toast (and spam the journal), and error-severity failures block the
9
+ /// Revit API thread behind the modal resolution dialog until a human clicks — past
10
+ /// the bridge's call budget. Attach() deletes warnings (recorded for the tool
11
+ /// result) and turns errors into a rollback, so a headless caller always gets an
12
+ /// answer instead of a hang.
13
+ /// </summary>
14
+ internal sealed class FailureGuard : IFailuresPreprocessor
15
+ {
16
+ public List<string> Warnings { get; } = new();
17
+ public List<string> Errors { get; } = new();
18
+
19
+ public static FailureGuard Attach(Transaction transaction)
20
+ {
21
+ var guard = new FailureGuard();
22
+ var options = transaction.GetFailureHandlingOptions();
23
+ options.SetFailuresPreprocessor(guard);
24
+ options.SetClearAfterRollback(true);
25
+ transaction.SetFailureHandlingOptions(options);
26
+ return guard;
27
+ }
28
+
29
+ public FailureProcessingResult PreprocessFailures(FailuresAccessor accessor)
30
+ {
31
+ bool hasError = false;
32
+ foreach (FailureMessageAccessor failure in accessor.GetFailureMessages())
33
+ {
34
+ string description;
35
+ try
36
+ {
37
+ description = failure.GetDescriptionText();
38
+ }
39
+ catch
40
+ {
41
+ description = "(failure without description)";
42
+ }
43
+
44
+ if (failure.GetSeverity() == FailureSeverity.Warning)
45
+ {
46
+ Warnings.Add(description);
47
+ accessor.DeleteWarning(failure);
48
+ }
49
+ else
50
+ {
51
+ hasError = true;
52
+ Errors.Add(description);
53
+ }
54
+ }
55
+ return hasError ? FailureProcessingResult.ProceedWithRollBack : FailureProcessingResult.Continue;
56
+ }
57
+
58
+ /// <summary>Formats the recorded errors for a commit-failure message.</summary>
59
+ public string DescribeErrors()
60
+ => Errors.Count > 0 ? $" Revit failure(s): {string.Join("; ", Errors)}." : string.Empty;
61
+ }
62
+ }
@@ -192,6 +192,7 @@ namespace RevitBridge.Tools
192
192
  private static void ApplyTemporaryIsolate(Document doc, View view, ICollection<ElementId> elementIds)
193
193
  {
194
194
  using var transaction = new Transaction(doc, "manage_selection: temporary isolate");
195
+ var failureGuard = FailureGuard.Attach(transaction);
195
196
  if (transaction.Start() != TransactionStatus.Started)
196
197
  throw new InvalidOperationException("Unable to start the temporary-isolate transaction.");
197
198
  try
@@ -201,7 +202,7 @@ namespace RevitBridge.Tools
201
202
  else
202
203
  view.IsolateElementsTemporary(elementIds);
203
204
  if (transaction.Commit() != TransactionStatus.Committed)
204
- throw new InvalidOperationException("The temporary-isolate transaction failed to commit.");
205
+ throw new InvalidOperationException("The temporary-isolate transaction failed to commit." + failureGuard.DescribeErrors());
205
206
  }
206
207
  catch (Autodesk.Revit.Exceptions.InvalidOperationException ex)
207
208
  {
@@ -19,7 +19,7 @@ namespace RevitBridge.Tools
19
19
 
20
20
  public string Name => "set_parameters";
21
21
  public string Label => "Set Parameters";
22
- public string Description => "Write parameter values on elements — also the home for rename: parameter 'Name' covers levels, views, sheets, types, etc. (falls back to the element's Name property when the Name parameter is read-only). updates apply in ONE transaction: partial success commits and lists the failures; if every update fails the transaction is rolled back and nothing changes. parameter accepts a display name (Comments, Mark, Name), a BuiltInParameter enum name (e.g. ALL_MODEL_MARK), or guid:<GUID> for a shared parameter; type parameters live on the element type, so pass the type's id. Values are validated against the parameter's storage type; numeric values are interpreted in the document's display units for that parameter unless 'unit' (e.g. millimeters, feet, squareMeters) is given.";
22
+ public string Description => "Write parameter values on elements — also the home for rename: parameter 'Name' covers levels, views, sheets, types, etc. (falls back to the element's Name property when the Name parameter is read-only). updates apply in ONE transaction: partial success commits and lists the failures; if every update fails the transaction is rolled back and nothing changes. parameter accepts a display name (Comments, Mark, Name), a BuiltInParameter enum name (e.g. ALL_MODEL_MARK), or guid:<GUID> for a shared parameter; type parameters live on the element type, so pass the type's id. Values are validated against the parameter's storage type; numeric values are interpreted in the document's display units for that parameter unless 'unit' (e.g. millimeters, feet, squareMeters) is given. Revit warnings raised at commit (e.g. duplicate Mark values) are auto-dismissed and listed in commitWarnings — mention them to the user; error-severity failures roll the whole transaction back.";
23
23
  public bool Write => true;
24
24
 
25
25
  public object ParametersSchema => new
@@ -64,6 +64,7 @@ namespace RevitBridge.Tools
64
64
  var failed = new List<Dictionary<string, object?>>();
65
65
 
66
66
  using var transaction = new Transaction(doc, "set_parameters");
67
+ var failureGuard = FailureGuard.Attach(transaction);
67
68
  if (transaction.Start() != TransactionStatus.Started)
68
69
  throw new InvalidOperationException("Unable to start the set_parameters transaction.");
69
70
 
@@ -95,7 +96,9 @@ namespace RevitBridge.Tools
95
96
  if (succeeded.Count > 0)
96
97
  {
97
98
  if (transaction.Commit() != TransactionStatus.Committed)
98
- throw new InvalidOperationException("The set_parameters transaction failed to commit; no changes were saved.");
99
+ throw new InvalidOperationException(
100
+ "Revit rejected the set_parameters commit and the transaction was rolled back; no changes were saved."
101
+ + failureGuard.DescribeErrors());
99
102
  }
100
103
  else
101
104
  {
@@ -118,6 +121,8 @@ namespace RevitBridge.Tools
118
121
  : succeeded.Count == 0
119
122
  ? $"No updates applied; all {failed.Count} failed — transaction rolled back.{failureSample}"
120
123
  : $"Updated {succeeded.Count} parameter value(s) on {elementCount} element(s); {failed.Count} failed.{failureSample}";
124
+ if (failureGuard.Warnings.Count > 0)
125
+ compact += $" {failureGuard.Warnings.Count} Revit warning(s) auto-dismissed at commit (see commitWarnings), e.g.: {failureGuard.Warnings[0]}";
121
126
 
122
127
  return new ToolOutput(new
123
128
  {
@@ -125,6 +130,7 @@ namespace RevitBridge.Tools
125
130
  committed = succeeded.Count > 0,
126
131
  succeeded,
127
132
  failed,
133
+ commitWarnings = failureGuard.Warnings,
128
134
  }, compact);
129
135
  }
130
136