@mrclrchtr/supi-extras 4.1.0 → 4.3.0

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 CHANGED
@@ -33,6 +33,7 @@ This package mixes a few commands and shortcuts with a few always-on UI tweaks.
33
33
  - `/exit` — exit pi
34
34
  - `/e` — alias for `/exit`
35
35
  - `/clear` — start a new session (alias for `/new`)
36
+ - `/clone-session <session-id>` — clone a session into the current worktree and switch to it; autocomplete searches IDs and session names
36
37
  - `/supi-stash` — browse, restore, copy, delete, or clear saved prompt drafts
37
38
 
38
39
  ## Shortcuts
@@ -83,6 +84,7 @@ That prevents git subprocesses from hanging while waiting for an interactive edi
83
84
  ## Source
84
85
 
85
86
  - `src/aliases.ts` — command aliases
87
+ - `src/clone-session.ts` — cross-worktree session cloning by ID
86
88
  - `src/prompt-stash.ts` — prompt stash shortcuts, persistence, and overlay
87
89
  - `src/skill-shortcut.ts` — `$skill-name` expansion and autocomplete
88
90
  - `src/tab-spinner.ts` — terminal tab-title spinner
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "4.1.0",
3
+ "version": "4.3.0",
4
4
  "description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
5
5
  "license": "MIT",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-extras",
3
- "version": "4.1.0",
3
+ "version": "4.3.0",
4
4
  "description": "SuPi extras — command aliases, skill shorthand, tab spinner, /supi-stash prompt stash with TUI overlay, and other small utilities",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,7 +33,7 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "clipboardy": "^5.3.1",
36
- "@mrclrchtr/supi-core": "4.1.0"
36
+ "@mrclrchtr/supi-core": "4.3.0"
37
37
  },
38
38
  "bundledDependencies": [
39
39
  "@mrclrchtr/supi-core"
@@ -0,0 +1,59 @@
1
+ import { type ExtensionAPI, SessionManager } from "@earendil-works/pi-coding-agent";
2
+
3
+ export default function cloneSession(pi: ExtensionAPI) {
4
+ let completionSessions: Awaited<ReturnType<typeof SessionManager.listAll>> | undefined;
5
+
6
+ pi.registerCommand("clone-session", {
7
+ description: "Clone a session by ID into this worktree and switch to it",
8
+ getArgumentCompletions: async (prefix) => {
9
+ completionSessions ??= await SessionManager.listAll();
10
+ const query = prefix.trim().toLowerCase();
11
+ const matches = completionSessions.filter(
12
+ (session) => session.id.startsWith(query) || session.name?.toLowerCase().includes(query),
13
+ );
14
+ return matches.length > 0
15
+ ? matches.map((session) => ({
16
+ value: session.id,
17
+ label: session.id,
18
+ description: [session.name, session.cwd].filter(Boolean).join(" — ") || undefined,
19
+ }))
20
+ : null;
21
+ },
22
+ handler: async (args, ctx) => {
23
+ const sessionId = args.trim();
24
+ if (!sessionId) {
25
+ ctx.ui.notify("Usage: /clone-session <session-id>", "warning");
26
+ return;
27
+ }
28
+
29
+ let sessionFile: string;
30
+ try {
31
+ const sourceSession = (await SessionManager.listAll()).find(
32
+ (session) => session.id === sessionId,
33
+ );
34
+ if (!sourceSession) {
35
+ ctx.ui.notify(`Session not found: ${sessionId}`, "warning");
36
+ return;
37
+ }
38
+
39
+ const clonedSessionFile = SessionManager.forkFrom(
40
+ sourceSession.path,
41
+ ctx.cwd,
42
+ ).getSessionFile();
43
+ if (!clonedSessionFile) throw new Error("cloned session was not persisted");
44
+ sessionFile = clonedSessionFile;
45
+ } catch (error) {
46
+ const message = error instanceof Error ? error.message : String(error);
47
+ ctx.ui.notify(`Could not clone session: ${message}`, "error");
48
+ return;
49
+ }
50
+
51
+ const result = await ctx.switchSession(sessionFile, {
52
+ withSession: async (nextCtx) => {
53
+ nextCtx.ui.notify("Session cloned into this worktree", "info");
54
+ },
55
+ });
56
+ if (result.cancelled) ctx.ui.notify(`Session cloned to ${sessionFile}`, "info");
57
+ },
58
+ });
59
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import aliases from "./aliases.ts";
2
+ import cloneSession from "./clone-session.ts";
2
3
  import copyPrompt from "./copy-prompt.ts";
3
4
  import gitEditor from "./git-editor.ts";
4
5
  import promptStash from "./prompt-stash.ts";
@@ -19,6 +20,7 @@ export default function (pi: Parameters<typeof tabSpinner>[0]) {
19
20
  tabSpinner(pi);
20
21
  promptStash(pi);
21
22
  copyPrompt(pi);
23
+ cloneSession(pi);
22
24
  gitEditor(pi);
23
25
  aliases(pi);
24
26
  skillShortcut(pi);
@@ -31,9 +31,15 @@ import {
31
31
  export default function supiFooter(pi: ExtensionAPI) {
32
32
  let currentModel: unknown;
33
33
  let requestRender: (() => void) | undefined;
34
+ let unregisterInvalidate: (() => void) | undefined;
34
35
 
35
36
  pi.on("session_start", (_event, ctx) => {
36
37
  currentModel = ctx.model;
38
+ if (!unregisterInvalidate) {
39
+ unregisterInvalidate = pi.events.on("supi:lsp:invalidate", () => {
40
+ requestRender?.();
41
+ });
42
+ }
37
43
  installFooter(ctx);
38
44
  });
39
45
 
@@ -42,17 +48,17 @@ export default function supiFooter(pi: ExtensionAPI) {
42
48
  requestRender?.();
43
49
  });
44
50
 
45
- pi.on("thinking_level_select", (_event, _ctx) => {
51
+ pi.on("thinking_level_select", () => {
46
52
  requestRender?.();
47
53
  });
48
54
 
49
55
  pi.on("session_shutdown", () => {
50
56
  currentModel = undefined;
51
57
  requestRender = undefined;
58
+ unregisterInvalidate?.();
59
+ unregisterInvalidate = undefined;
52
60
  });
53
61
 
54
- // ---- footer installation ----
55
-
56
62
  // biome-ignore lint/suspicious/noExplicitAny: ctx type from pi session_start handler is complex
57
63
  function installFooter(ctx: any) {
58
64
  ctx.ui.setFooter(
@@ -151,8 +157,6 @@ export default function supiFooter(pi: ExtensionAPI) {
151
157
  theme.fg("dim", statsLeft) + theme.fg("dim", laidOut.padding) + laidOut.styled;
152
158
 
153
159
  const lines = [pwdLine, truncateToWidth(statsLine, width, theme.fg("dim", "..."))];
154
-
155
- // Extension statuses
156
160
  buildStatusLine(lines, footerData, width, theme);
157
161
 
158
162
  return lines;