pi-profiles-manager 1.1.4 → 1.1.6

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.6",
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
  }));
@@ -265,8 +266,8 @@ describe("package extension", () => {
265
266
  );
266
267
  });
267
268
 
268
- it.each(["return", "escape"])(
269
- "keeps export confirmation open until %s dismisses it",
269
+ it.each(["return", "escape", "a", "space"])(
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,27 @@ describe("package extension", () => {
320
321
  "piprofile:1:eyJfdHlwZSI6InBpcHJvZmlsZSIsInZlcnNpb24iOjEsInByb2ZpbGUiOnsibmFtZSI6ImFscGhhIiwib3JkZXIiOjAsImZhdm9yaXRlIjpmYWxzZX19",
321
322
  );
322
323
  expect(tuiState.texts).toContainEqual([
323
- "Copied profile 'alpha' to clipboard.",
324
+ "📤 Profile Export: 'alpha'",
325
+ 1,
326
+ 0,
327
+ ]);
328
+ expect(tuiState.texts).toContainEqual([
329
+ "Select string below to copy:",
330
+ 1,
331
+ 0,
332
+ ]);
333
+ expect(tuiState.texts).toContainEqual([
334
+ "piprofile:1:eyJfdHlwZSI6InBpcHJvZmlsZSIsInZlcnNpb24iOjEsInByb2ZpbGUiOnsibmFtZSI6ImFscGhhIiwib3JkZXIiOjAsImZhdm9yaXRlIjpmYWxzZX19",
335
+ 1,
336
+ 0,
337
+ ]);
338
+ expect(tuiState.texts).toContainEqual([
339
+ "[ Press any key or Esc to close ]",
340
+ 1,
341
+ 0,
342
+ ]);
343
+ expect(tuiState.texts).toContainEqual([
344
+ "✓ Copy command sent to terminal clipboard.",
324
345
  1,
325
346
  0,
326
347
  ]);
@@ -331,19 +352,19 @@ describe("package extension", () => {
331
352
  expect(confirmationView).toBeDefined();
332
353
  expect(handlerSettled).toBe(false);
333
354
 
334
- confirmationView!.handleInput(dismissKey);
355
+ confirmationView?.handleInput(dismissKey);
335
356
  await handling;
336
357
 
337
358
  expect(finishConfirmation).toBeDefined();
338
359
  expect(handlerSettled).toBe(true);
339
360
  expect(ctx.ui.notify).not.toHaveBeenCalledWith(
340
- "Copied profile 'alpha' to clipboard.",
361
+ " Copy command sent to terminal clipboard.",
341
362
  "info",
342
363
  );
343
364
  },
344
365
  );
345
366
 
346
- it("reports clipboard failures without showing export confirmation", async () => {
367
+ it("keeps export string visible and shows error if clipboard fails", async () => {
347
368
  const syncFs = await import("node:fs");
348
369
  vi.mocked(syncFs.readFileSync).mockReturnValue(JSON.stringify({
349
370
  version: 1,
@@ -358,10 +379,27 @@ describe("package extension", () => {
358
379
  const handler = (vi.mocked(pi.registerCommand).mock.calls as any[]).find(
359
380
  ([name]: [string]) => name === "profiles",
360
381
  )[1].handler as (args: string, ctx: any) => Promise<void>;
382
+
383
+ let confirmationView: { handleInput(data: string): void } | undefined;
384
+ let finishConfirmation: ((value: unknown) => void) | undefined;
361
385
  const custom = vi
362
386
  .fn()
363
387
  .mockResolvedValueOnce("alpha")
364
388
  .mockResolvedValueOnce("export")
389
+ .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) => {
390
+ return await new Promise((resolve) => {
391
+ finishConfirmation = resolve;
392
+ confirmationView = factory(
393
+ { requestRender: vi.fn() },
394
+ {
395
+ fg: (_color: string, text: string) => text,
396
+ bold: (text: string) => text,
397
+ },
398
+ {},
399
+ resolve,
400
+ );
401
+ });
402
+ })
365
403
  .mockResolvedValueOnce(null);
366
404
  const ctx = {
367
405
  sessionManager: { getSessionId: () => "session-1" },
@@ -369,22 +407,33 @@ describe("package extension", () => {
369
407
  ui: { custom, notify: vi.fn(), setStatus: vi.fn() },
370
408
  };
371
409
 
372
- await handler("", ctx);
410
+ let handlerSettled = false;
411
+ const handling = handler("", ctx).then(() => { handlerSettled = true; });
412
+ await vi.waitFor(() => expect(custom).toHaveBeenCalledTimes(3));
373
413
 
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.",
414
+ expect(tuiState.texts).toContainEqual([
415
+ "📤 Profile Export: 'alpha'",
381
416
  1,
382
417
  0,
383
418
  ]);
384
- expect(ctx.ui.notify).not.toHaveBeenCalledWith(
385
- "Copied profile 'alpha' to clipboard.",
386
- "info",
387
- );
419
+ expect(tuiState.texts).toContainEqual([
420
+ "piprofile:1:eyJfdHlwZSI6InBpcHJvZmlsZSIsInZlcnNpb24iOjEsInByb2ZpbGUiOnsibmFtZSI6ImFscGhhIiwib3JkZXIiOjAsImZhdm9yaXRlIjpmYWxzZX19",
421
+ 1,
422
+ 0,
423
+ ]);
424
+ expect(tuiState.texts).toContainEqual([
425
+ expect.stringContaining("Failed to copy: clipboard unavailable"),
426
+ 1,
427
+ 0,
428
+ ]);
429
+ expect(confirmationView).toBeDefined();
430
+ expect(handlerSettled).toBe(false);
431
+
432
+ confirmationView?.handleInput("return");
433
+ await handling;
434
+
435
+ expect(finishConfirmation).toBeDefined();
436
+ expect(handlerSettled).toBe(true);
388
437
  });
389
438
 
390
439
  it("imports typed profile strings into the authoritative config without legacy storage", async () => {
package/src/extension.ts CHANGED
@@ -174,17 +174,34 @@ 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", "Copying 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(new Text(theme.fg("accent", theme.bold(`📤 Profile Export: '${profileName}'`)), 1, 0));
184
+ container.addChild(statusText);
185
+ container.addChild(new Text(theme.fg("dim", "Select string below to copy:"), 1, 0));
186
+ container.addChild(new Text(theme.fg("accent", exportString), 1, 0));
187
+ container.addChild(new Text(theme.fg("dim", "[ Press any key or Esc to close ]"), 1, 0));
182
188
  container.addChild(new DynamicBorder((value: string) => theme.fg("accent", value)));
189
+
190
+ copyPromise.then(() => {
191
+ statusText.setText(theme.fg("accent", "✓ Copy command sent to terminal clipboard."));
192
+ container.invalidate();
193
+ tui.requestRender();
194
+ }).catch((error) => {
195
+ statusText.setText(theme.fg("error", `Failed to copy: ${error instanceof Error ? error.message : String(error)}`));
196
+ container.invalidate();
197
+ tui.requestRender();
198
+ });
199
+
183
200
  return {
184
201
  render: (width: number) => container.render(width),
185
202
  invalidate: () => container.invalidate(),
186
203
  handleInput: (data: string) => {
187
- if (matchesKey(data, "return") || matchesKey(data, "escape")) done();
204
+ done();
188
205
  tui.requestRender();
189
206
  },
190
207
  };
@@ -509,12 +526,9 @@ export default function extension(pi: PiLike) {
509
526
  favorite: selected === manager.config.defaultProfile,
510
527
  },
511
528
  })).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
- }
529
+ const exportString = `piprofile:1:${encoded}`;
530
+ const copyPromise = copyToClipboard(exportString);
531
+ await showExportDialog(ctx, selected, exportString, copyPromise);
518
532
  continue;
519
533
  }
520
534
  if (action === "delete") {