pi-profiles-manager 1.1.4 → 1.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-profiles-manager",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Interactive SDD model profile management built natively for the Pi Coding Agent.",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -34,6 +34,7 @@ vi.mock("@earendil-works/pi-tui", () => ({
34
34
  },
35
35
  Text: class {
36
36
  constructor(...args: unknown[]) { tuiState.texts.push(args); }
37
+ setText(text: string) { tuiState.texts.push([text, 1, 0]); }
37
38
  },
38
39
  matchesKey: (data: string, key: string) => data === key,
39
40
  }));
@@ -266,7 +267,7 @@ describe("package extension", () => {
266
267
  });
267
268
 
268
269
  it.each(["return", "escape"])(
269
- "keeps export confirmation open until %s dismisses it",
270
+ "keeps export string visible until %s dismisses it",
270
271
  async (dismissKey) => {
271
272
  const syncFs = await import("node:fs");
272
273
  vi.mocked(syncFs.readFileSync).mockReturnValue(JSON.stringify({
@@ -290,7 +291,7 @@ describe("package extension", () => {
290
291
  .fn()
291
292
  .mockResolvedValueOnce("alpha")
292
293
  .mockResolvedValueOnce("export")
293
- .mockImplementationOnce(async (factory: any, options: unknown) => {
294
+ .mockImplementationOnce(async (factory: (tui: { requestRender: () => void }, theme: { fg: (color: string, text: string) => string; bold: (text: string) => string }, kb: unknown, done: (value: unknown) => void) => { handleInput(data: string): void }, options: unknown) => {
294
295
  confirmationOptions = options;
295
296
  return await new Promise((resolve) => {
296
297
  finishConfirmation = resolve;
@@ -320,7 +321,12 @@ describe("package extension", () => {
320
321
  "piprofile:1:eyJfdHlwZSI6InBpcHJvZmlsZSIsInZlcnNpb24iOjEsInByb2ZpbGUiOnsibmFtZSI6ImFscGhhIiwib3JkZXIiOjAsImZhdm9yaXRlIjpmYWxzZX19",
321
322
  );
322
323
  expect(tuiState.texts).toContainEqual([
323
- "Copied profile 'alpha' to clipboard.",
324
+ "piprofile:1:eyJfdHlwZSI6InBpcHJvZmlsZSIsInZlcnNpb24iOjEsInByb2ZpbGUiOnsibmFtZSI6ImFscGhhIiwib3JkZXIiOjAsImZhdm9yaXRlIjpmYWxzZX19",
325
+ 1,
326
+ 0,
327
+ ]);
328
+ expect(tuiState.texts).toContainEqual([
329
+ expect.stringContaining("Copied profile 'alpha' to clipboard"),
324
330
  1,
325
331
  0,
326
332
  ]);
@@ -331,7 +337,7 @@ describe("package extension", () => {
331
337
  expect(confirmationView).toBeDefined();
332
338
  expect(handlerSettled).toBe(false);
333
339
 
334
- confirmationView!.handleInput(dismissKey);
340
+ confirmationView?.handleInput(dismissKey);
335
341
  await handling;
336
342
 
337
343
  expect(finishConfirmation).toBeDefined();
@@ -343,7 +349,7 @@ describe("package extension", () => {
343
349
  },
344
350
  );
345
351
 
346
- it("reports clipboard failures without showing export confirmation", async () => {
352
+ it("keeps export string visible and shows error if clipboard fails", async () => {
347
353
  const syncFs = await import("node:fs");
348
354
  vi.mocked(syncFs.readFileSync).mockReturnValue(JSON.stringify({
349
355
  version: 1,
@@ -358,10 +364,27 @@ describe("package extension", () => {
358
364
  const handler = (vi.mocked(pi.registerCommand).mock.calls as any[]).find(
359
365
  ([name]: [string]) => name === "profiles",
360
366
  )[1].handler as (args: string, ctx: any) => Promise<void>;
367
+
368
+ let confirmationView: { handleInput(data: string): void } | undefined;
369
+ let finishConfirmation: ((value: unknown) => void) | undefined;
361
370
  const custom = vi
362
371
  .fn()
363
372
  .mockResolvedValueOnce("alpha")
364
373
  .mockResolvedValueOnce("export")
374
+ .mockImplementationOnce(async (factory: (tui: { requestRender: () => void }, theme: { fg: (color: string, text: string) => string; bold: (text: string) => string }, kb: unknown, done: (value: unknown) => void) => { handleInput(data: string): void }, _options: unknown) => {
375
+ return await new Promise((resolve) => {
376
+ finishConfirmation = resolve;
377
+ confirmationView = factory(
378
+ { requestRender: vi.fn() },
379
+ {
380
+ fg: (_color: string, text: string) => text,
381
+ bold: (text: string) => text,
382
+ },
383
+ {},
384
+ resolve,
385
+ );
386
+ });
387
+ })
365
388
  .mockResolvedValueOnce(null);
366
389
  const ctx = {
367
390
  sessionManager: { getSessionId: () => "session-1" },
@@ -369,22 +392,28 @@ describe("package extension", () => {
369
392
  ui: { custom, notify: vi.fn(), setStatus: vi.fn() },
370
393
  };
371
394
 
372
- await handler("", ctx);
395
+ let handlerSettled = false;
396
+ const handling = handler("", ctx).then(() => { handlerSettled = true; });
397
+ await vi.waitFor(() => expect(custom).toHaveBeenCalledTimes(3));
373
398
 
374
- expect(ctx.ui.notify).toHaveBeenCalledWith(
375
- "Failed to copy profile 'alpha' to clipboard: clipboard unavailable",
376
- "error",
377
- );
378
- expect(custom).toHaveBeenCalledTimes(3);
379
- expect(tuiState.texts).not.toContainEqual([
380
- "Copied profile 'alpha' to clipboard.",
399
+ expect(tuiState.texts).toContainEqual([
400
+ "piprofile:1:eyJfdHlwZSI6InBpcHJvZmlsZSIsInZlcnNpb24iOjEsInByb2ZpbGUiOnsibmFtZSI6ImFscGhhIiwib3JkZXIiOjAsImZhdm9yaXRlIjpmYWxzZX19",
381
401
  1,
382
402
  0,
383
403
  ]);
384
- expect(ctx.ui.notify).not.toHaveBeenCalledWith(
385
- "Copied profile 'alpha' to clipboard.",
386
- "info",
387
- );
404
+ expect(tuiState.texts).toContainEqual([
405
+ expect.stringContaining("Failed to copy profile 'alpha' to clipboard"),
406
+ 1,
407
+ 0,
408
+ ]);
409
+ expect(confirmationView).toBeDefined();
410
+ expect(handlerSettled).toBe(false);
411
+
412
+ confirmationView?.handleInput("return");
413
+ await handling;
414
+
415
+ expect(finishConfirmation).toBeDefined();
416
+ expect(handlerSettled).toBe(true);
388
417
  });
389
418
 
390
419
  it("imports typed profile strings into the authoritative config without legacy storage", async () => {
package/src/extension.ts CHANGED
@@ -174,12 +174,26 @@ function prompt(ctx: ContextLike, title: string, initial = ""): Promise<string |
174
174
  }, { overlay: true });
175
175
  }
176
176
 
177
- function showCopyConfirmation(ctx: ContextLike, message: string): Promise<void> {
178
- return (ctx.ui.custom as any)((tui: any, theme: any, _kb: any, done: any) => {
179
- const container = new Container() as any;
177
+ function showExportDialog(ctx: ContextLike, profileName: string, exportString: string, copyPromise: Promise<void>): Promise<void> {
178
+ return (ctx.ui.custom as (factory: unknown, options?: unknown) => Promise<void>)((tui: { requestRender: () => void }, theme: { fg: (color: string, text: string) => string; bold: (text: string) => string }, _kb: unknown, done: (value: void) => void) => {
179
+ const container = new Container() as { addChild: (child: unknown) => void; render: (width: number) => unknown; invalidate: () => void };
180
+ const statusText = new Text(theme.fg("accent", theme.bold(`Copying profile '${profileName}' to clipboard...`)), 1, 0);
181
+
180
182
  container.addChild(new DynamicBorder((value: string) => theme.fg("accent", value)));
181
- container.addChild(new Text(theme.fg("accent", theme.bold(message)), 1, 0));
183
+ container.addChild(statusText);
184
+ container.addChild(new Text(theme.fg("accent", exportString), 1, 0));
182
185
  container.addChild(new DynamicBorder((value: string) => theme.fg("accent", value)));
186
+
187
+ copyPromise.then(() => {
188
+ statusText.setText(theme.fg("accent", theme.bold(`Copied profile '${profileName}' to clipboard.`)));
189
+ container.invalidate();
190
+ tui.requestRender();
191
+ }).catch((error) => {
192
+ statusText.setText(theme.fg("error", theme.bold(`Failed to copy profile '${profileName}' to clipboard: ${error instanceof Error ? error.message : String(error)}`)));
193
+ container.invalidate();
194
+ tui.requestRender();
195
+ });
196
+
183
197
  return {
184
198
  render: (width: number) => container.render(width),
185
199
  invalidate: () => container.invalidate(),
@@ -509,12 +523,9 @@ export default function extension(pi: PiLike) {
509
523
  favorite: selected === manager.config.defaultProfile,
510
524
  },
511
525
  })).toString("base64");
512
- try {
513
- await copyToClipboard(`piprofile:1:${encoded}`);
514
- await showCopyConfirmation(ctx, `Copied profile '${selected}' to clipboard.`);
515
- } catch (error) {
516
- ctx.ui.notify(`Failed to copy profile '${selected}' to clipboard: ${error instanceof Error ? error.message : String(error)}`, "error");
517
- }
526
+ const exportString = `piprofile:1:${encoded}`;
527
+ const copyPromise = copyToClipboard(exportString);
528
+ await showExportDialog(ctx, selected, exportString, copyPromise);
518
529
  continue;
519
530
  }
520
531
  if (action === "delete") {