pi-ui-extend 0.1.27 → 0.1.29

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.
@@ -11,6 +11,8 @@ export function setFileLinkOpenerTestDeps(overrides) {
11
11
  };
12
12
  }
13
13
  export function openFileLink(link) {
14
+ if (isWebUrl(link.url))
15
+ return openPathWithSystemViewer(link.url);
14
16
  const filePath = link.filePath ?? filePathFromUrl(link.url);
15
17
  if (!filePath)
16
18
  return false;
@@ -19,6 +21,9 @@ export function openFileLink(link) {
19
21
  return true;
20
22
  return openPathWithSystemViewer(filePath);
21
23
  }
24
+ function isWebUrl(url) {
25
+ return url.startsWith("http://") || url.startsWith("https://");
26
+ }
22
27
  function filePathFromUrl(url) {
23
28
  if (!url.startsWith("file://"))
24
29
  return undefined;
@@ -3,10 +3,11 @@ import { homedir } from "node:os";
3
3
  import { isAbsolute, resolve } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  const FILE_PATH_CANDIDATE = /(?<![\p{L}\p{N}_:])((?:file:\/\/\/|~[\\/]|\.{1,2}[\\/]|[A-Za-z]:[\\/]|[\\/]|[A-Za-z0-9_.@-]+[\\/])[^\s"'`<>]*)/gu;
6
+ const WEB_URL_CANDIDATE = /https?:\/\/[^\s"'`<>]+/gu;
6
7
  const TRAILING_PUNCTUATION = new Set([".", ",", ";", ")", "]", "}"]);
7
8
  export function detectFileLinks(text, cwd) {
8
9
  const links = [];
9
- if (!text.includes("/") && !text.includes("\\"))
10
+ if (!text.includes("/") && !text.includes("\\") && !text.includes("http://") && !text.includes("https://"))
10
11
  return links;
11
12
  for (const match of text.matchAll(FILE_PATH_CANDIDATE)) {
12
13
  const raw = match[1];
@@ -28,6 +29,17 @@ export function detectFileLinks(text, cwd) {
28
29
  column: location.column,
29
30
  });
30
31
  }
32
+ for (const match of text.matchAll(WEB_URL_CANDIDATE)) {
33
+ const raw = match[0];
34
+ const candidate = trimTrailingPunctuation(raw);
35
+ if (!candidate)
36
+ continue;
37
+ links.push({
38
+ start: match.index,
39
+ end: match.index + candidate.length,
40
+ url: candidate,
41
+ });
42
+ }
31
43
  return mergeOverlappingLinks(links);
32
44
  }
33
45
  export function hyperlink(text, url) {
@@ -139,6 +139,7 @@ export declare class AppMouseController {
139
139
  private inputClickFlashRegionForEvent;
140
140
  private imageTargetAt;
141
141
  private fileLinkAt;
142
+ private fileLinkTargetAt;
142
143
  private statusTargetAt;
143
144
  private handleImageClick;
144
145
  private handleFileLinkClick;
@@ -60,6 +60,8 @@ export class AppMouseController {
60
60
  this.showClickFlashOnPress(event);
61
61
  if (event.button === 0 && !event.released && this.handleInputBorderStatusClick(event))
62
62
  return;
63
+ if (event.button === 0 && !event.released && this.fileLinkAt(event))
64
+ return;
63
65
  if (this.handleMouseSelection(event))
64
66
  return;
65
67
  if (this.withClickFlash(event, () => this.handleImageClick(event)))
@@ -234,9 +236,9 @@ export class AppMouseController {
234
236
  const imageTarget = this.imageTargetAt(event);
235
237
  if (imageTarget)
236
238
  return { y: event.y, startColumn: imageTarget.start + 1, endColumn: imageTarget.end + 1 };
237
- const link = this.fileLinkAt(event);
238
- if (link)
239
- return { y: event.y, startColumn: link.start + 1, endColumn: link.end + 1 };
239
+ const linkTarget = this.fileLinkTargetAt(event);
240
+ if (linkTarget)
241
+ return { y: event.y, startColumn: linkTarget.startColumn, endColumn: linkTarget.endColumn };
240
242
  const tabTarget = this.tabLineTargetAt(event);
241
243
  if (tabTarget)
242
244
  return { y: tabTarget.row, startColumn: tabTarget.startColumn, endColumn: tabTarget.endColumn };
@@ -290,10 +292,19 @@ export class AppMouseController {
290
292
  return targets?.find((candidate) => event.x >= candidate.start + 1 && event.x <= candidate.end);
291
293
  }
292
294
  fileLinkAt(event) {
295
+ return this.fileLinkTargetAt(event)?.link;
296
+ }
297
+ fileLinkTargetAt(event) {
293
298
  const text = this.renderedRowTexts.get(event.y);
294
299
  if (!text)
295
300
  return undefined;
296
- return detectFileLinks(text, this.host.cwd()).find((candidate) => event.x >= candidate.start + 1 && event.x <= candidate.end);
301
+ for (const link of detectFileLinks(text, this.host.cwd())) {
302
+ const startColumn = stringDisplayWidth(text.slice(0, link.start)) + 1;
303
+ const endColumn = startColumn + stringDisplayWidth(text.slice(link.start, link.end));
304
+ if (event.x >= startColumn && event.x < endColumn)
305
+ return { link, startColumn, endColumn };
306
+ }
307
+ return undefined;
297
308
  }
298
309
  statusTargetAt(event) {
299
310
  const target = [
@@ -323,7 +334,7 @@ export class AppMouseController {
323
334
  };
324
335
  }
325
336
  handleImageClick(event) {
326
- if (event.button !== 0 || !event.released)
337
+ if (!isPrimaryButtonRelease(event))
327
338
  return false;
328
339
  const imageTarget = this.imageTargetAt(event);
329
340
  if (!imageTarget)
@@ -341,7 +352,7 @@ export class AppMouseController {
341
352
  }
342
353
  handleFileLinkClick(event) {
343
354
  const modifiedPress = isModifiedPrimaryButton(event.button) && !event.released;
344
- const plainRelease = event.button === 0 && event.released;
355
+ const plainRelease = isPrimaryButtonRelease(event);
345
356
  if (!modifiedPress && !plainRelease)
346
357
  return false;
347
358
  const link = this.fileLinkAt(event);
@@ -1053,6 +1064,9 @@ function isModifiedPrimaryButton(button) {
1053
1064
  const modifierBits = button & (8 | 16);
1054
1065
  return primaryButton && modifierBits !== 0;
1055
1066
  }
1067
+ function isPrimaryButtonRelease(event) {
1068
+ return event.released && (event.button === 0 || (event.button & 3) === 3);
1069
+ }
1056
1070
  function editorLayoutRows(terminalRows, tabPanelRows) {
1057
1071
  return Math.max(1, terminalRows - tabPanelRows);
1058
1072
  }
@@ -70,6 +70,10 @@ export function firstUserMessageText(ctx: ExtensionContext): string | undefined
70
70
  return undefined;
71
71
  }
72
72
 
73
+ function hasExistingUserMessage(ctx: ExtensionContext): boolean {
74
+ return firstUserMessageText(ctx) !== undefined;
75
+ }
76
+
73
77
  export function fallbackSessionTitleFromInput(input: string, maxTitleChars: number): string | undefined {
74
78
  const normalized = input
75
79
  .replace(/[\t\r\n]+/gu, " ")
@@ -371,24 +375,6 @@ export default function sessionTitle(pi: ExtensionAPI) {
371
375
  })();
372
376
  }
373
377
 
374
- function primeTitleGenerationFromExistingSession(ctx: ExtensionContext, currentConfig: SessionTitleConfig): void {
375
- if (currentSessionName(ctx)) return;
376
-
377
- const input = firstUserMessageText(ctx);
378
- if (!input) return;
379
- if (!currentConfig.enabled) {
380
- applyFallbackSessionTitle(ctx, currentConfig, input);
381
- return;
382
- }
383
-
384
- pendingGeneration = {
385
- sessionId: ctx.sessionManager.getSessionId(),
386
- input: truncateInput(input, currentConfig.maxInputChars),
387
- attempts: 0,
388
- };
389
- startTitleGeneration(ctx, currentConfig);
390
- }
391
-
392
378
  function isSameSessionPath(left: string | undefined, right: string | undefined): boolean {
393
379
  if (!left || !right) return false;
394
380
  if (left === right) return true;
@@ -447,7 +433,6 @@ export default function sessionTitle(pi: ExtensionAPI) {
447
433
  await prepareForkTitleState(event, ctx);
448
434
  refreshSessionUi(ctx, { force: true });
449
435
  scheduleSessionUiRefresh(ctx);
450
- if (!forkTitleState) primeTitleGenerationFromExistingSession(ctx, config);
451
436
  });
452
437
 
453
438
  pi.on("session_shutdown", async () => {
@@ -480,6 +465,10 @@ export default function sessionTitle(pi: ExtensionAPI) {
480
465
  sessionId = currentSessionId;
481
466
  const currentName = currentSessionName(ctx);
482
467
  const activeForkTitleState = forkTitleState?.sessionId === currentSessionId ? forkTitleState : undefined;
468
+ if (!activeForkTitleState && hasExistingUserMessage(ctx)) {
469
+ forkTitleState = undefined;
470
+ return { action: "continue" as const };
471
+ }
483
472
  if (currentName && (!activeForkTitleState || currentName !== activeForkTitleState.inheritedSessionName)) {
484
473
  forkTitleState = undefined;
485
474
  return { action: "continue" as const };
@@ -1,6 +1,7 @@
1
1
  export const DEFAULT_STARTUP_TIMEOUT_MS = 45_000;
2
2
  export const DEFAULT_DIAGNOSTICS_WAIT_MS = 10_000;
3
3
  export const DEFAULT_MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024;
4
+ export const DEFAULT_IDLE_SHUTDOWN_MS = 30_000;
4
5
  export const REQUEST_TIMEOUT_MS = 30_000;
5
6
  export const SHUTDOWN_WRITE_TIMEOUT_MS = 100;
6
7
  export const SHUTDOWN_TERM_TIMEOUT_MS = 2_000;
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { DEFAULT_DIAGNOSTICS_WAIT_MS, DEFAULT_MAX_FILE_SIZE_BYTES, LSP_MANAGER_GLOBAL_KEY } from "./constants";
3
+ import { DEFAULT_DIAGNOSTICS_WAIT_MS, DEFAULT_IDLE_SHUTDOWN_MS, DEFAULT_MAX_FILE_SIZE_BYTES, LSP_MANAGER_GLOBAL_KEY } from "./constants";
4
4
  import { DiagnosticsStore } from "./diagnostics-store";
5
5
  import { LspClient } from "./client";
6
6
  import { loadLspConfig } from "./_shared/config";
@@ -43,12 +43,51 @@ export class LspManager {
43
43
  private readonly diagnostics = new DiagnosticsStore();
44
44
  private readonly clients = new Map<string, LspClient>();
45
45
  private readonly backoff = new Map<string, { retryAt: number; attempts: number; reason: string }>();
46
+ private idleShutdownTimer: ReturnType<typeof setTimeout> | undefined;
47
+ private activeOperations = 0;
48
+ private handlingSignal = false;
46
49
  private readonly handleProcessExit = () => {
47
50
  this.shutdownAllSync();
48
51
  };
52
+ private readonly handleProcessSignal = (signal: NodeJS.Signals) => {
53
+ this.shutdownAllSync();
54
+
55
+ // Restore the platform default for the terminating signal. LSP servers are
56
+ // spawned detached so they can otherwise outlive Pi when the process is
57
+ // killed by a terminal/editor without a session_shutdown event.
58
+ if (this.handlingSignal) return;
59
+ this.handlingSignal = true;
60
+ process.kill(process.pid, signal);
61
+ };
49
62
 
50
63
  constructor() {
51
64
  process.once("exit", this.handleProcessExit);
65
+ process.once("SIGINT", this.handleProcessSignal);
66
+ process.once("SIGTERM", this.handleProcessSignal);
67
+ process.once("SIGHUP", this.handleProcessSignal);
68
+ }
69
+
70
+ private clearIdleShutdownTimer(): void {
71
+ if (!this.idleShutdownTimer) return;
72
+ clearTimeout(this.idleShutdownTimer);
73
+ this.idleShutdownTimer = undefined;
74
+ }
75
+
76
+ private beginOperation(): void {
77
+ this.activeOperations += 1;
78
+ this.clearIdleShutdownTimer();
79
+ }
80
+
81
+ private endOperation(): void {
82
+ this.activeOperations = Math.max(0, this.activeOperations - 1);
83
+ if (this.activeOperations > 0 || this.clients.size === 0) return;
84
+
85
+ this.clearIdleShutdownTimer();
86
+ this.idleShutdownTimer = setTimeout(() => {
87
+ if (this.activeOperations > 0 || this.clients.size === 0) return;
88
+ void this.shutdownAll();
89
+ }, DEFAULT_IDLE_SHUTDOWN_MS);
90
+ this.idleShutdownTimer.unref?.();
52
91
  }
53
92
 
54
93
  async matchingServers(ctx: ExtensionContext, file: string): Promise<{ matches: MatchedServer[]; warnings: string[]; workspace: string }> {
@@ -104,93 +143,98 @@ export class LspManager {
104
143
  }
105
144
 
106
145
  async updateDiagnosticsForFile(ctx: ExtensionContext, file: string): Promise<string> {
107
- const { matches, warnings, workspace } = await this.matchingServers(ctx, file);
108
- if (matches.length === 0) return formatWarnings("LSP diagnostics", warnings);
109
-
110
- const lines: string[] = [];
111
- for (const match of matches) {
112
- try {
113
- const maxFileSizeBytes = match.server.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
114
- if (!(await fileSizeAllowed(file, maxFileSizeBytes))) {
115
- lines.push(`${LSP_DIAGNOSTIC_ICON} ${match.server.id}: skipped ${match.relFile}; file exceeds maxFileSizeBytes (${maxFileSizeBytes})`);
116
- continue;
117
- }
146
+ this.beginOperation();
147
+ try {
148
+ const { matches, warnings, workspace } = await this.matchingServers(ctx, file);
149
+ if (matches.length === 0) return formatWarnings("LSP diagnostics", warnings);
118
150
 
119
- // Clear stale diagnostics before refreshing this file. The synchronous
120
- // wait below must observe a fresh publishDiagnostics notification, not an
121
- // old error from a previous document version. Empty diagnostics published
122
- // by the server are stored, but this local clear is not.
123
- this.diagnostics.clear(match.server.id, match.root, filePathToUri(file));
124
-
125
- const text = await readTextFile(file);
126
- const client = await this.getClient(match.server, match.root, file, workspace, ctx.signal);
127
- const languageId = languageIdForFile(match.server, file);
128
- const startedAt = Date.now();
129
- const doc = await client.openOrChange(file, languageId, text, ctx.signal);
130
- await client.didSave(file);
131
- const diagnosticsWaitMs = match.server.diagnosticsWaitMs ?? DEFAULT_DIAGNOSTICS_WAIT_MS;
132
-
133
- // typescript-language-server sometimes does not emit a fresh
134
- // textDocument/publishDiagnostics notification after didChange/didSave,
135
- // even though tsserver can answer diagnostics synchronously. Prefer the
136
- // explicit tsserver request when the server exposes it, so post-edit
137
- // diagnostics don't degrade into a misleading publishDiagnostics timeout.
138
- let tsserverFallbackError: string | undefined;
151
+ const lines: string[] = [];
152
+ for (const match of matches) {
139
153
  try {
140
- const tsserverDiagnostics = await client.tsserverDiagnostics(file, text, diagnosticsWaitMs, ctx.signal);
141
- if (tsserverDiagnostics !== undefined) {
142
- const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, tsserverDiagnostics);
143
- this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
144
- lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
154
+ const maxFileSizeBytes = match.server.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
155
+ if (!(await fileSizeAllowed(file, maxFileSizeBytes))) {
156
+ lines.push(`${LSP_DIAGNOSTIC_ICON} ${match.server.id}: skipped ${match.relFile}; file exceeds maxFileSizeBytes (${maxFileSizeBytes})`);
145
157
  continue;
146
158
  }
147
- } catch (error) {
148
- tsserverFallbackError = (error as Error).message;
149
- }
150
159
 
151
- let pullDiagnosticsError: string | undefined;
152
- if (match.server.pullDiagnostics !== false) {
160
+ // Clear stale diagnostics before refreshing this file. The synchronous
161
+ // wait below must observe a fresh publishDiagnostics notification, not an
162
+ // old error from a previous document version. Empty diagnostics published
163
+ // by the server are stored, but this local clear is not.
164
+ this.diagnostics.clear(match.server.id, match.root, filePathToUri(file));
165
+
166
+ const text = await readTextFile(file);
167
+ const client = await this.getClient(match.server, match.root, file, workspace, ctx.signal);
168
+ const languageId = languageIdForFile(match.server, file);
169
+ const startedAt = Date.now();
170
+ const doc = await client.openOrChange(file, languageId, text, ctx.signal);
171
+ await client.didSave(file);
172
+ const diagnosticsWaitMs = match.server.diagnosticsWaitMs ?? DEFAULT_DIAGNOSTICS_WAIT_MS;
173
+
174
+ // typescript-language-server sometimes does not emit a fresh
175
+ // textDocument/publishDiagnostics notification after didChange/didSave,
176
+ // even though tsserver can answer diagnostics synchronously. Prefer the
177
+ // explicit tsserver request when the server exposes it, so post-edit
178
+ // diagnostics don't degrade into a misleading publishDiagnostics timeout.
179
+ let tsserverFallbackError: string | undefined;
153
180
  try {
154
- const pulledDiagnostics = await client.pullDiagnostics(file, diagnosticsWaitMs, ctx.signal);
155
- if (pulledDiagnostics !== undefined) {
156
- const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, pulledDiagnostics);
181
+ const tsserverDiagnostics = await client.tsserverDiagnostics(file, text, diagnosticsWaitMs, ctx.signal);
182
+ if (tsserverDiagnostics !== undefined) {
183
+ const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, tsserverDiagnostics);
157
184
  this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
158
185
  lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
159
186
  continue;
160
187
  }
161
188
  } catch (error) {
162
- pullDiagnosticsError = (error as Error).message;
189
+ tsserverFallbackError = (error as Error).message;
163
190
  }
164
- }
165
191
 
166
- if (match.server.waitForPublishDiagnostics === false || diagnosticsWaitMs <= 0) {
167
- continue;
168
- }
192
+ let pullDiagnosticsError: string | undefined;
193
+ if (match.server.pullDiagnostics !== false) {
194
+ try {
195
+ const pulledDiagnostics = await client.pullDiagnostics(file, diagnosticsWaitMs, ctx.signal);
196
+ if (pulledDiagnostics !== undefined) {
197
+ const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, pulledDiagnostics);
198
+ this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
199
+ lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
200
+ continue;
201
+ }
202
+ } catch (error) {
203
+ pullDiagnosticsError = (error as Error).message;
204
+ }
205
+ }
206
+
207
+ if (match.server.waitForPublishDiagnostics === false || diagnosticsWaitMs <= 0) {
208
+ continue;
209
+ }
169
210
 
170
- const entry = await this.diagnostics.waitForFile(
171
- match.server.id,
172
- match.root,
173
- file,
174
- startedAt,
175
- doc.version,
176
- diagnosticsWaitMs,
177
- ctx.signal,
178
- );
179
- if (!isFreshDiagnosticsEntry(entry, startedAt, doc.version)) {
180
- const fallbackSuffix = tsserverFallbackError ? `; tsserver fallback failed: ${tsserverFallbackError}` : "";
181
- const pullSuffix = pullDiagnosticsError ? `; pull diagnostics failed: ${pullDiagnosticsError}` : "";
182
- lines.push(`${LSP_DIAGNOSTIC_ICON} ${match.server.id}: timed out after ${diagnosticsWaitMs}ms waiting for fresh diagnostics for ${match.relFile}${fallbackSuffix}${pullSuffix}`);
183
- continue;
211
+ const entry = await this.diagnostics.waitForFile(
212
+ match.server.id,
213
+ match.root,
214
+ file,
215
+ startedAt,
216
+ doc.version,
217
+ diagnosticsWaitMs,
218
+ ctx.signal,
219
+ );
220
+ if (!isFreshDiagnosticsEntry(entry, startedAt, doc.version)) {
221
+ const fallbackSuffix = tsserverFallbackError ? `; tsserver fallback failed: ${tsserverFallbackError}` : "";
222
+ const pullSuffix = pullDiagnosticsError ? `; pull diagnostics failed: ${pullDiagnosticsError}` : "";
223
+ lines.push(`${LSP_DIAGNOSTIC_ICON} ${match.server.id}: timed out after ${diagnosticsWaitMs}ms waiting for fresh diagnostics for ${match.relFile}${fallbackSuffix}${pullSuffix}`);
224
+ continue;
225
+ }
226
+ const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, entry.diagnostics);
227
+ if (diagnostics !== entry.diagnostics) this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
228
+ lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
229
+ } catch (error) {
230
+ lines.push(`${LSP_DIAGNOSTIC_ICON} ${match.server.id}: ${(error as Error).message}`);
184
231
  }
185
- const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, entry.diagnostics);
186
- if (diagnostics !== entry.diagnostics) this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
187
- lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
188
- } catch (error) {
189
- lines.push(`${LSP_DIAGNOSTIC_ICON} ${match.server.id}: ${(error as Error).message}`);
190
232
  }
191
- }
192
233
 
193
- return [formatWarnings("LSP diagnostics", warnings), joinSections("LSP diagnostics", lines)].filter(Boolean).join("\n\n");
234
+ return [formatWarnings("LSP diagnostics", warnings), joinSections("LSP diagnostics", lines)].filter(Boolean).join("\n\n");
235
+ } finally {
236
+ this.endOperation();
237
+ }
194
238
  }
195
239
 
196
240
  async ensureDocumentForTool(ctx: ExtensionContext, inputPath: string): Promise<{ file: string; match: MatchedServer; client: LspClient; workspace: string } | undefined> {
@@ -215,15 +259,20 @@ export class LspManager {
215
259
  }
216
260
 
217
261
  async shutdownAll(): Promise<void> {
262
+ this.clearIdleShutdownTimer();
218
263
  const clients = [...this.clients.values()];
219
264
  this.clients.clear();
220
265
  await Promise.allSettled(clients.map((client) => client.shutdown()));
221
266
  }
222
267
 
223
268
  shutdownAllSync(): void {
269
+ this.clearIdleShutdownTimer();
224
270
  const clients = [...this.clients.values()];
225
271
  this.clients.clear();
226
272
  process.off("exit", this.handleProcessExit);
273
+ process.off("SIGINT", this.handleProcessSignal);
274
+ process.off("SIGTERM", this.handleProcessSignal);
275
+ process.off("SIGHUP", this.handleProcessSignal);
227
276
  for (const client of clients) client.shutdownSync();
228
277
  }
229
278
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ui-extend",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {