pi-project-switcher 0.8.0 → 0.8.1
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/README.md +1 -1
- package/index.ts +73 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ A [pi coding agent](https://github.com/earendil-works/pi) extension to switch be
|
|
|
15
15
|
- sets the session display name
|
|
16
16
|
- injects the project path into every agent turn's system prompt, so file operations default to the active project
|
|
17
17
|
- **via the Telegram bridge**: the switch is confirmed in the chat — a short reply with the project, working directory, and session identity, plus buttons (project list, and switch back to the previous project). The confirmation is sent from the new session runtime, so it also works when the switch restores a stored session. No-switch outcomes (already active, cancelled, unknown) are answered in the chat too.
|
|
18
|
-
- **Telegram transport re-arm**: a session-replacing switch from the Telegram bridge
|
|
18
|
+
- **Telegram transport re-arm**: a session-replacing switch from the Telegram bridge loses the bridge's transport: pi-telegram stands down on session shutdown, and its reconnect cannot take over the lock across the restored session's different cwd (same-pid locks never go stale; same-process takeover requires matching cwd). The switcher therefore transitions the transport across the switch itself: before the session switch it executes `/telegram-disconnect` (which releases pi-telegram's lock), and after the switch it re-executes `/telegram-connect` from the new session (~3 s delay) — but **only** when the session being left provably owned the connected transport: pi-telegram's lock (`~/.pi/agent/tmp/telegram/owners.json`) must name this process with a fresh heartbeat and a cwd matching the old session. A cancelled switch reconnects immediately. Native switches and switches from sessions that didn't own the bot never touch the transport. The lock file is only read, never modified.
|
|
19
19
|
- **if the project doesn't exist yet**, offers to create the folder and switch to it (confirmation dialog on dialog-capable surfaces; use `/project <name>!` to skip the dialog — e.g. on headless/RPC surfaces). Unsafe names (path segments, `..`, hidden, absolute) are never created.
|
|
20
20
|
- **Session restore** — a machine-local map (`~/.pi/agent/project-switcher-sessions.json`) remembers the most recent session per project. Switching projects returns you to that project's last session; if none exists (or the file is gone), the switch happens in the current session.
|
|
21
21
|
- **Auto-detection** — if pi starts inside `~/dev/<project>`, that project is active automatically
|
package/index.ts
CHANGED
|
@@ -356,13 +356,39 @@ function telegramOwnersPath(): string {
|
|
|
356
356
|
const TELEGRAM_OWNERSHIP_FRESH_MS = 10_000;
|
|
357
357
|
|
|
358
358
|
/**
|
|
359
|
-
* Delay before re-dispatching /telegram-connect
|
|
360
|
-
* switch
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
* non-forced acquire can replace across a cwd mismatch.
|
|
359
|
+
* Delay before re-dispatching /telegram-connect in the new runtime. The
|
|
360
|
+
* pre-switch /telegram-disconnect already released the lock synchronously,
|
|
361
|
+
* so the connect succeeds unconditionally; the short delay only avoids
|
|
362
|
+
* racing the session-replacement lifecycle observers.
|
|
364
363
|
*/
|
|
365
|
-
const TELEGRAM_REARM_DELAY_MS =
|
|
364
|
+
const TELEGRAM_REARM_DELAY_MS = 3_000;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Release the Telegram transport from the OLD (owning) runtime, before
|
|
368
|
+
* the session is replaced. pi's prompt() executes extension commands
|
|
369
|
+
* synchronously, so when this returns, pi-telegram has stopped polling and
|
|
370
|
+
* released the lock (deleted the owners.json entry). Only the owning
|
|
371
|
+
* runtime can release (release() is ownership-guarded), which is exactly
|
|
372
|
+
* why this must run before ctx.switchSession() — and why the probe must
|
|
373
|
+
* pass first. Threaded Mode is disabled for this bot, so no confirmation
|
|
374
|
+
* dialog is raised in the headless daemon.
|
|
375
|
+
*/
|
|
376
|
+
async function releaseTelegramTransportBeforeSwitch(pi: any, source: string): Promise<boolean> {
|
|
377
|
+
try {
|
|
378
|
+
await pi.sendUserMessage("/telegram-disconnect", {
|
|
379
|
+
expandPromptTemplates: true,
|
|
380
|
+
});
|
|
381
|
+
return true;
|
|
382
|
+
} catch (err: any) {
|
|
383
|
+
try {
|
|
384
|
+
pi.notify?.(`Telegram release before ${source} switch failed: ${err?.message ?? err}`, "warning");
|
|
385
|
+
} catch {
|
|
386
|
+
// Non-fatal: the switch proceeds; a later manual /telegram-connect
|
|
387
|
+
// still works because we never mutated anything.
|
|
388
|
+
}
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
366
392
|
|
|
367
393
|
/**
|
|
368
394
|
* Read-only probe: is the session we are about to leave the live owner of
|
|
@@ -412,8 +438,10 @@ function scheduleTelegramRearm(newCtx: any, source: string): void {
|
|
|
412
438
|
pendingRearmTimer = null;
|
|
413
439
|
try {
|
|
414
440
|
// Command re-dispatch from the FRESH runtime: executes pi-telegram's
|
|
415
|
-
// connect handler (no agent turn)
|
|
416
|
-
//
|
|
441
|
+
// connect handler (no agent turn). The lock was released by the
|
|
442
|
+
// pre-switch disconnect, so the acquire succeeds unconditionally —
|
|
443
|
+
// no staleness wait, no takeover dialog (same-pid locks never go
|
|
444
|
+
// stale, and same-process takeover cannot cross a cwd mismatch).
|
|
417
445
|
await newCtx.sendUserMessage("/telegram-connect", {
|
|
418
446
|
expandPromptTemplates: true,
|
|
419
447
|
});
|
|
@@ -686,13 +714,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
686
714
|
const branchStr = branch ? ` on branch \`${branch}\`` : "";
|
|
687
715
|
// Ownership probe BEFORE the switch (the old session's cwd is only
|
|
688
716
|
// available here): when the session we are leaving is the live owner
|
|
689
|
-
// of the connected Telegram transport, the
|
|
690
|
-
//
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
//
|
|
717
|
+
// of the connected Telegram transport, the transport must be carried
|
|
718
|
+
// across the replacement: the old runtime releases it now (the only
|
|
719
|
+
// runtime that CAN — release() is ownership-guarded), and the new
|
|
720
|
+
// runtime re-arms it in withSession. Without the release, the lock
|
|
721
|
+
// entry would stay forever active-here (same-pid locks never go
|
|
722
|
+
// stale) and the connect could never re-acquire across the cwd
|
|
723
|
+
// change. The probe failing means we are NOT the owner (another pi
|
|
724
|
+
// instance, or Telegram was never connected here): touch nothing.
|
|
694
725
|
const ownedTelegramTransport =
|
|
695
726
|
isTelegramOrigin && probeTelegramTransportOwnership(ctx.cwd);
|
|
727
|
+
let releasedTelegramTransport = false;
|
|
728
|
+
if (ownedTelegramTransport) {
|
|
729
|
+
// Must run while the old runtime is still current, i.e. before
|
|
730
|
+
// ctx.switchSession(). Executes /telegram-disconnect synchronously
|
|
731
|
+
// (pi runs extension commands before any queue/streaming logic).
|
|
732
|
+
releasedTelegramTransport = await releaseTelegramTransportBeforeSwitch(pi, name);
|
|
733
|
+
}
|
|
696
734
|
const result = await ctx.switchSession(targetSession, {
|
|
697
735
|
withSession: async (newCtx: any) => {
|
|
698
736
|
// Safety net in case the restored session has no project entry:
|
|
@@ -723,11 +761,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
723
761
|
{ deliverAs: "followUp" }
|
|
724
762
|
);
|
|
725
763
|
}
|
|
726
|
-
if (
|
|
727
|
-
// Re-arm the Telegram transport from the fresh runtime.
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
//
|
|
764
|
+
if (releasedTelegramTransport) {
|
|
765
|
+
// Re-arm the Telegram transport from the fresh runtime. The lock
|
|
766
|
+
// was released by the pre-switch disconnect, so this connect
|
|
767
|
+
// acquires unconditionally after the short safety delay. Only
|
|
768
|
+
// the withSession context is touched (see scheduleTelegramRearm).
|
|
731
769
|
scheduleTelegramRearm(newCtx, name);
|
|
732
770
|
}
|
|
733
771
|
},
|
|
@@ -738,6 +776,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
738
776
|
// attempted; an explicit later status/switch overrides it.
|
|
739
777
|
activeProject = previous;
|
|
740
778
|
ctx.ui.notify(`Switch cancelled. Staying on ${previous ?? "no project"}.`, "info");
|
|
779
|
+
if (releasedTelegramTransport) {
|
|
780
|
+
// The pre-switch disconnect released the transport for the
|
|
781
|
+
// replacement that is now NOT happening: reconnect from this
|
|
782
|
+
// (still-current) runtime so the cancelled switch leaves the
|
|
783
|
+
// transport in its original state. No lock remains, so the
|
|
784
|
+
// connect re-acquires unconditionally.
|
|
785
|
+
try {
|
|
786
|
+
await pi.sendUserMessage("/telegram-connect", {
|
|
787
|
+
expandPromptTemplates: true,
|
|
788
|
+
});
|
|
789
|
+
} catch (err: any) {
|
|
790
|
+
ctx.ui.notify(
|
|
791
|
+
`Telegram reconnect after cancelled switch failed: ${err?.message ?? err}`,
|
|
792
|
+
"warning"
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
741
796
|
if (isTelegramOrigin) {
|
|
742
797
|
await ctx.waitForIdle();
|
|
743
798
|
pi.sendUserMessage(
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name": "pi-project-switcher", "version": "0.8.
|
|
1
|
+
{"name": "pi-project-switcher", "version": "0.8.1", "description": "pi coding agent extension: switch between projects under a configurable base directory via /project", "main": "index.ts", "type": "module", "scripts": {"test": "vitest run", "test:watch": "vitest", "typecheck": "tsc --noEmit"}, "keywords": ["pi", "pi-package", "pi-extension", "project", "switcher", "project-switching"], "author": "stefclawd", "license": "MIT", "repository": {"type": "git", "url": "git+https://github.com/stefclawd/pi-project-switcher.git"}, "bugs": {"url": "https://github.com/stefclawd/pi-project-switcher/issues"}, "homepage": "https://github.com/stefclawd/pi-project-switcher#readme", "files": ["index.ts", "README.md", "LICENSE"], "engines": {"node": ">=22.19.0"}, "pi": {"extensions": ["./index.ts"]}, "devDependencies": {"@earendil-works/pi-coding-agent": "^0.85.1", "@types/node": "^24.0.0", "typescript": "^5.7.0", "vitest": "^3.0.0"}}
|