pi-interactive-shell 0.5.0 → 0.5.2
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 +11 -0
- package/index.ts +53 -19
- package/package.json +1 -1
- package/scripts/install.js +31 -29
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the `pi-interactive-shell` extension will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.5.2] - 2026-01-23
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **npx installation missing files** - The install script had a hardcoded file list that was missing 4 critical files (`key-encoding.ts`, `types.ts`, `tool-schema.ts`, `reattach-overlay.ts`). Now reads from `package.json`'s `files` array as the single source of truth, ensuring all files are always copied.
|
|
9
|
+
- **Broken symlink handling** - Fixed skill symlink creation failing when a broken symlink already existed at the target path. `existsSync()` returns `false` for broken symlinks, causing the old code to skip removal. Now unconditionally attempts removal, correctly handling broken symlinks.
|
|
10
|
+
|
|
11
|
+
## [0.5.1] - 2026-01-22
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **Prevent overlay stacking** - Starting a new `interactive_shell` session or using `/attach` while an overlay is already open now returns an error instead of causing undefined behavior with stacked/stuck overlays.
|
|
15
|
+
|
|
5
16
|
## [0.5.0] - 2026-01-22
|
|
6
17
|
|
|
7
18
|
### Changed
|
package/index.ts
CHANGED
|
@@ -8,6 +8,9 @@ import { translateInput } from "./key-encoding.js";
|
|
|
8
8
|
import { TOOL_NAME, TOOL_LABEL, TOOL_DESCRIPTION, toolParameters, type ToolParams } from "./tool-schema.js";
|
|
9
9
|
import { formatDuration, formatDurationMs } from "./types.js";
|
|
10
10
|
|
|
11
|
+
// Track whether an overlay is currently open to prevent stacking
|
|
12
|
+
let overlayOpen = false;
|
|
13
|
+
|
|
11
14
|
export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
12
15
|
pi.on("session_shutdown", () => {
|
|
13
16
|
sessionManager.killAll();
|
|
@@ -370,12 +373,24 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
370
373
|
const config = loadConfig(effectiveCwd);
|
|
371
374
|
const isHandsFree = mode === "hands-free";
|
|
372
375
|
|
|
376
|
+
// Prevent starting a new overlay while one is already open
|
|
377
|
+
if (overlayOpen) {
|
|
378
|
+
return {
|
|
379
|
+
content: [{ type: "text", text: "An interactive shell overlay is already open. Wait for it to close or kill the active session before starting a new one." }],
|
|
380
|
+
isError: true,
|
|
381
|
+
details: { error: "overlay_already_open" },
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
373
385
|
// Generate sessionId early so it's available immediately
|
|
374
386
|
const generatedSessionId = isHandsFree ? generateSessionId(name) : undefined;
|
|
375
387
|
|
|
376
388
|
// For hands-free mode: non-blocking - return immediately with sessionId
|
|
377
389
|
// Agent can then query status/output via sessionId and kill when done
|
|
378
390
|
if (isHandsFree && generatedSessionId) {
|
|
391
|
+
// Mark overlay as open
|
|
392
|
+
overlayOpen = true;
|
|
393
|
+
|
|
379
394
|
// Start overlay but don't await - it runs in background
|
|
380
395
|
const overlayPromise = ctx.ui.custom<InteractiveShellResult>(
|
|
381
396
|
(tui, theme, _kb, done) =>
|
|
@@ -421,12 +436,14 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
421
436
|
|
|
422
437
|
// Handle overlay completion in background (cleanup when user closes)
|
|
423
438
|
overlayPromise.then((result) => {
|
|
439
|
+
overlayOpen = false;
|
|
424
440
|
// Session already handles cleanup via finishWith* methods
|
|
425
441
|
// This just ensures the promise doesn't cause unhandled rejection
|
|
426
442
|
if (result.userTookOver) {
|
|
427
443
|
// User took over - session continues interactively
|
|
428
444
|
}
|
|
429
445
|
}).catch(() => {
|
|
446
|
+
overlayOpen = false;
|
|
430
447
|
// Ignore errors - session cleanup handles this
|
|
431
448
|
});
|
|
432
449
|
|
|
@@ -448,6 +465,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
448
465
|
}
|
|
449
466
|
|
|
450
467
|
// Interactive mode: blocking - wait for overlay to close
|
|
468
|
+
overlayOpen = true;
|
|
451
469
|
onUpdate?.({
|
|
452
470
|
content: [{ type: "text", text: `Opening: ${command}` }],
|
|
453
471
|
details: {
|
|
@@ -457,7 +475,9 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
457
475
|
},
|
|
458
476
|
});
|
|
459
477
|
|
|
460
|
-
|
|
478
|
+
let result: InteractiveShellResult;
|
|
479
|
+
try {
|
|
480
|
+
result = await ctx.ui.custom<InteractiveShellResult>(
|
|
461
481
|
(tui, theme, _kb, done) =>
|
|
462
482
|
new InteractiveShellOverlay(
|
|
463
483
|
tui,
|
|
@@ -532,6 +552,9 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
532
552
|
},
|
|
533
553
|
},
|
|
534
554
|
);
|
|
555
|
+
} finally {
|
|
556
|
+
overlayOpen = false;
|
|
557
|
+
}
|
|
535
558
|
|
|
536
559
|
let summary: string;
|
|
537
560
|
if (result.backgrounded) {
|
|
@@ -569,6 +592,12 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
569
592
|
pi.registerCommand("attach", {
|
|
570
593
|
description: "Reattach to a background shell session",
|
|
571
594
|
handler: async (args, ctx) => {
|
|
595
|
+
// Prevent reattaching while another overlay is open
|
|
596
|
+
if (overlayOpen) {
|
|
597
|
+
ctx.ui.notify("An overlay is already open. Close it first.", "error");
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
572
601
|
const sessions = sessionManager.list();
|
|
573
602
|
|
|
574
603
|
if (sessions.length === 0) {
|
|
@@ -601,25 +630,30 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
601
630
|
}
|
|
602
631
|
|
|
603
632
|
const config = loadConfig(ctx.cwd);
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
633
|
+
overlayOpen = true;
|
|
634
|
+
try {
|
|
635
|
+
await ctx.ui.custom<InteractiveShellResult>(
|
|
636
|
+
(tui, theme, _kb, done) =>
|
|
637
|
+
new ReattachOverlay(
|
|
638
|
+
tui,
|
|
639
|
+
theme,
|
|
640
|
+
{ id: session.id, command: session.command, reason: session.reason, session: session.session },
|
|
641
|
+
config,
|
|
642
|
+
done,
|
|
643
|
+
),
|
|
644
|
+
{
|
|
645
|
+
overlay: true,
|
|
646
|
+
overlayOptions: {
|
|
647
|
+
width: `${config.overlayWidthPercent}%`,
|
|
648
|
+
maxHeight: `${config.overlayHeightPercent}%`,
|
|
649
|
+
anchor: "center",
|
|
650
|
+
margin: 1,
|
|
651
|
+
},
|
|
620
652
|
},
|
|
621
|
-
|
|
622
|
-
|
|
653
|
+
);
|
|
654
|
+
} finally {
|
|
655
|
+
overlayOpen = false;
|
|
656
|
+
}
|
|
623
657
|
},
|
|
624
658
|
});
|
|
625
659
|
}
|
package/package.json
CHANGED
package/scripts/install.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { existsSync, mkdirSync, cpSync, symlinkSync, unlinkSync, readFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, cpSync, symlinkSync, unlinkSync, readFileSync, statSync } from "node:fs";
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { execSync } from "node:child_process";
|
|
@@ -24,36 +24,34 @@ function main() {
|
|
|
24
24
|
log(`Creating ${EXTENSION_DIR}`);
|
|
25
25
|
mkdirSync(EXTENSION_DIR, { recursive: true });
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"index.ts",
|
|
31
|
-
"config.ts",
|
|
32
|
-
"overlay-component.ts",
|
|
33
|
-
"pty-session.ts",
|
|
34
|
-
"session-manager.ts",
|
|
35
|
-
"README.md",
|
|
36
|
-
"SKILL.md",
|
|
37
|
-
"CHANGELOG.md",
|
|
38
|
-
];
|
|
27
|
+
// Read files list from package.json (single source of truth)
|
|
28
|
+
// Include package.json itself (npm auto-includes it but it's not in the files array)
|
|
29
|
+
const files = ["package.json", ...(pkg.files || [])];
|
|
39
30
|
|
|
40
|
-
// Copy files
|
|
41
|
-
for (const
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
31
|
+
// Copy files and directories
|
|
32
|
+
for (const rawEntry of files) {
|
|
33
|
+
// Normalize: remove trailing slashes for consistent handling
|
|
34
|
+
const entry = rawEntry.replace(/\/+$/, "");
|
|
35
|
+
const src = join(packageRoot, entry);
|
|
36
|
+
const dest = join(EXTENSION_DIR, entry);
|
|
37
|
+
|
|
38
|
+
if (!existsSync(src)) {
|
|
39
|
+
continue;
|
|
47
40
|
}
|
|
48
|
-
}
|
|
49
41
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
42
|
+
try {
|
|
43
|
+
const stat = statSync(src);
|
|
44
|
+
if (stat.isDirectory()) {
|
|
45
|
+
mkdirSync(dest, { recursive: true });
|
|
46
|
+
cpSync(src, dest, { recursive: true });
|
|
47
|
+
log(`Copied ${entry}/`);
|
|
48
|
+
} else {
|
|
49
|
+
cpSync(src, dest);
|
|
50
|
+
log(`Copied ${entry}`);
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
log(`Warning: Could not copy ${entry}: ${error.message}`);
|
|
54
|
+
}
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
// Run npm install in extension directory
|
|
@@ -72,8 +70,12 @@ function main() {
|
|
|
72
70
|
const skillTarget = join(EXTENSION_DIR, "SKILL.md");
|
|
73
71
|
|
|
74
72
|
try {
|
|
75
|
-
if (
|
|
73
|
+
// Remove existing entry if present (handles regular files, symlinks, and broken symlinks)
|
|
74
|
+
// Note: existsSync returns false for broken symlinks, so we unconditionally try unlink
|
|
75
|
+
try {
|
|
76
76
|
unlinkSync(skillLink);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
if (e.code !== "ENOENT") throw e;
|
|
77
79
|
}
|
|
78
80
|
symlinkSync(skillTarget, skillLink);
|
|
79
81
|
log("Skill symlink created");
|